From 785e5b2c16c6f1f29e950903de833f5720803c91 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Mar 2025 17:41:03 +0000 Subject: [PATCH 001/258] Bump version to 2025.4.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 b9695c350a7..67cac936cc0 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index a85b3d99c67..ef17b753954 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0.dev0" +version = "2025.4.0b0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 8a43e8af9e6276b0ec15f970f39bf1d6b2111128 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 26 Mar 2025 23:34:53 +0100 Subject: [PATCH 002/258] Fix refresh state for Comelit alarm (#141370) --- .../components/comelit/alarm_control_panel.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/comelit/alarm_control_panel.py b/homeassistant/components/comelit/alarm_control_panel.py index 5ecc9a63599..1ad26905dd1 100644 --- a/homeassistant/components/comelit/alarm_control_panel.py +++ b/homeassistant/components/comelit/alarm_control_panel.py @@ -41,6 +41,7 @@ ALARM_ACTIONS: dict[str, str] = { ALARM_AREA_ARMED_STATUS: dict[str, int] = { + DISABLE: 0, HOME_P1: 1, HOME_P2: 2, NIGHT: 3, @@ -128,20 +129,38 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED, }.get(self._area.human_status) + async def _async_update_state(self, area_state: AlarmAreaState, armed: int) -> None: + """Update state after action.""" + self._area.human_status = area_state + self._area.armed = armed + await self.async_update_ha_state() + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if code != str(self._api.device_pin): return await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE]) + await self._async_update_state( + AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE] + ) async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY]) + await self._async_update_state( + AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY] + ) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME]) + await self._async_update_state( + AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1] + ) async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT]) + await self._async_update_state( + AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT] + ) From d568209bd5a7f6f6db83f506a215d7a7e3854709 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 26 Mar 2025 19:36:04 +0100 Subject: [PATCH 003/258] Bump deebot-client to 12.4.0 (#141501) --- 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 6d3dc5c9be6..acb5b620719 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==12.3.1"] + "requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 08bf975f23e..d7db5450a5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -758,7 +758,7 @@ debugpy==1.8.13 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==12.3.1 +deebot-client==12.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9cadd834d53..229c1a76559 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -649,7 +649,7 @@ dbus-fast==2.43.0 debugpy==1.8.13 # homeassistant.components.ecovacs -deebot-client==12.3.1 +deebot-client==12.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 25b45ce867a556ab716b597c984a572e1f76aa31 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Mar 2025 20:52:39 +0100 Subject: [PATCH 004/258] Sort SmartThings devices to be created by parent device id (#141515) --- homeassistant/components/smartthings/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index ab7df490bd3..20325e7d3e5 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -410,7 +410,9 @@ def create_devices( rooms: dict[str, str], ) -> None: """Create devices in the device registry.""" - for device in devices.values(): + for device in sorted( + devices.values(), key=lambda d: d.device.parent_device_id or "" + ): kwargs: dict[str, Any] = {} if device.device.hub is not None: kwargs = { From 35bcc9d5af410c8164d179c56818652814d80ad8 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 26 Mar 2025 22:03:24 +0100 Subject: [PATCH 005/258] Show box for Smartthings rise number entity (#141526) --- homeassistant/components/smartthings/number.py | 3 ++- tests/components/smartthings/snapshots/test_number.ambr | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smartthings/number.py b/homeassistant/components/smartthings/number.py index bb21520e271..2f2ac7903f2 100644 --- a/homeassistant/components/smartthings/number.py +++ b/homeassistant/components/smartthings/number.py @@ -4,7 +4,7 @@ from __future__ import annotations from pysmartthings import Attribute, Capability, Command, SmartThings -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import NumberEntity, NumberMode from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -32,6 +32,7 @@ class SmartThingsWasherRinseCyclesNumberEntity(SmartThingsEntity, NumberEntity): _attr_translation_key = "washer_rinse_cycles" _attr_native_step = 1.0 + _attr_mode = NumberMode.BOX def __init__(self, client: SmartThings, device: FullDevice) -> None: """Initialize the instance.""" diff --git a/tests/components/smartthings/snapshots/test_number.ambr b/tests/components/smartthings/snapshots/test_number.ambr index a5954a98cf3..66aade5b958 100644 --- a/tests/components/smartthings/snapshots/test_number.ambr +++ b/tests/components/smartthings/snapshots/test_number.ambr @@ -7,7 +7,7 @@ 'capabilities': dict({ 'max': 5, 'min': 0, - 'mode': , + 'mode': , 'step': 1.0, }), 'config_entry_id': , @@ -44,7 +44,7 @@ 'friendly_name': 'Washer Rinse cycles', 'max': 5, 'min': 0, - 'mode': , + 'mode': , 'step': 1.0, 'unit_of_measurement': 'cycles', }), @@ -64,7 +64,7 @@ 'capabilities': dict({ 'max': 5, 'min': 0, - 'mode': , + 'mode': , 'step': 1.0, }), 'config_entry_id': , @@ -101,7 +101,7 @@ 'friendly_name': 'Washing Machine Rinse cycles', 'max': 5, 'min': 0, - 'mode': , + 'mode': , 'step': 1.0, 'unit_of_measurement': 'cycles', }), From ca6286f241edb3cdf823221cb0edad2733eed781 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 26 Mar 2025 22:16:26 +0100 Subject: [PATCH 006/258] Fix work area sensor for Husqvarna Automower (#141527) * Fix work area sensor for Husqvarna Automower * simplify --- .../components/husqvarna_automower/sensor.py | 10 +++++++--- tests/components/husqvarna_automower/test_sensor.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 75af24ee0ee..d7a83c82185 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -227,12 +227,16 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]: @callback def _get_current_work_area_name(data: MowerAttributes) -> str: """Return the name of the current work area.""" - if data.mower.work_area_id is None: - return STATE_NO_WORK_AREA_ACTIVE if TYPE_CHECKING: # Sensor does not get created if values are None assert data.work_areas is not None - return data.work_areas[data.mower.work_area_id].name + if ( + data.mower.work_area_id is not None + and data.mower.work_area_id in data.work_areas + ): + return data.work_areas[data.mower.work_area_id].name + + return STATE_NO_WORK_AREA_ACTIVE @callback diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 08ed5251344..85d20178e73 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -110,6 +110,18 @@ async def test_work_area_sensor( state = hass.states.get("sensor.test_mower_1_work_area") assert state.state == "my_lawn" + # Test EPOS mower, which returns work_area_id = 0, when no + # work area is active and has no default work_area_id=0 + values[TEST_MOWER_ID].mower.work_area_id = 0 + del values[TEST_MOWER_ID].work_areas[0] + del values[TEST_MOWER_ID].work_area_dict[0] + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mower_1_work_area") + assert state.state == "no_work_area_active" + @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( From e04d8557ae3ebe4d041087b787acaed808365c24 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Mar 2025 22:56:57 +0100 Subject: [PATCH 007/258] Fix MQTT options flow QoS selector can not serialize (#141528) --- homeassistant/components/mqtt/config_flow.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 471b6d048a7..5f0984e9b9f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -153,7 +153,6 @@ from .util import ( learn_more_url, valid_birth_will, valid_publish_topic, - valid_qos_schema, valid_subscribe_topic, valid_subscribe_topic_template, ) @@ -182,7 +181,6 @@ PASSWORD_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWO QOS_SELECTOR = NumberSelector( NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=2) ) -QOS_DATA_SCHEMA = vol.All(QOS_SELECTOR, valid_qos_schema) KEEPALIVE_SELECTOR = vol.All( NumberSelector( NumberSelectorConfig( @@ -1145,7 +1143,7 @@ class MQTTOptionsFlowHandler(OptionsFlow): "birth_payload", description={"suggested_value": birth[CONF_PAYLOAD]} ) ] = TEXT_SELECTOR - fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_DATA_SCHEMA + fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_SELECTOR fields[vol.Optional("birth_retain", default=birth[ATTR_RETAIN])] = ( BOOLEAN_SELECTOR ) @@ -1168,7 +1166,7 @@ class MQTTOptionsFlowHandler(OptionsFlow): "will_payload", description={"suggested_value": will[CONF_PAYLOAD]} ) ] = TEXT_SELECTOR - fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_DATA_SCHEMA + fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_SELECTOR fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = ( BOOLEAN_SELECTOR ) From cff0a632e8e343245e26135d525a26af787a5918 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Mar 2025 23:35:28 +0100 Subject: [PATCH 008/258] Fix QoS schema issue in MQTT subentries (#141531) --- homeassistant/components/mqtt/config_flow.py | 8 ++------ tests/components/mqtt/test_config_flow.py | 4 ++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 5f0984e9b9f..7fe01e9a890 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1267,13 +1267,9 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): reconfig=True, ) if user_input is not None: - merged_user_input, errors = validate_user_input( - user_input, MQTT_DEVICE_PLATFORM_FIELDS - ) + _, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS) if not errors: - self._subentry_data[CONF_DEVICE] = cast( - MqttDeviceData, merged_user_input - ) + self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, user_input) if self.source == SOURCE_RECONFIGURE: return await self.async_step_summary_menu() return await self.async_step_entity() diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 2635263ae8e..c94d692b374 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -2908,6 +2908,10 @@ async def test_subentry_configflow( iter(config_subentries_data["components"].values()) ) + subentry_device_data = next(iter(config_entry.subentries.values())).data["device"] + for option, value in mock_device_user_input.items(): + assert subentry_device_data[option] == value + await hass.async_block_till_done() From 89c3266c7ee07d3811f6e3a1bb9f0326f9d257b8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Mar 2025 23:21:26 +0000 Subject: [PATCH 009/258] Bump version to 2025.4.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 67cac936cc0..98032451838 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index ef17b753954..0361152b89c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b0" +version = "2025.4.0b1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 6b9e8c301b6a7deb4e7baf9a923fccc753a9d339 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 27 Mar 2025 00:24:14 +0100 Subject: [PATCH 010/258] Fix wrong friendly name for `storage_power` in `solaredge` (#141269) * Fix wrong friendly name for `storage_power` in `solaredge` "Stored power" is a contradiction in itself. You can only store energy. * Two additional spelling fixes * Sentence-case "site" --- homeassistant/components/solaredge/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/solaredge/strings.json b/homeassistant/components/solaredge/strings.json index 2b626987546..105a9282a6d 100644 --- a/homeassistant/components/solaredge/strings.json +++ b/homeassistant/components/solaredge/strings.json @@ -5,7 +5,7 @@ "title": "Define the API parameters for this installation", "data": { "name": "The name of this installation", - "site_id": "The SolarEdge site-id", + "site_id": "The SolarEdge site ID", "api_key": "[%key:common::config_flow::data::api_key%]" } } @@ -14,7 +14,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", "site_not_active": "The site is not active", - "could_not_connect": "Could not connect to the solaredge API" + "could_not_connect": "Could not connect to the SolarEdge API" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" @@ -65,7 +65,7 @@ "name": "Grid power" }, "storage_power": { - "name": "Stored power" + "name": "Storage power" }, "purchased_energy": { "name": "Imported energy" From b1ee5a76e10813bedb6059574a548cb8b4946397 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Thu, 27 Mar 2025 07:46:58 +0100 Subject: [PATCH 011/258] Support for upcoming pyLoad-ng release in pyLoad integration (#141297) Fix extra key `proxy` in pyLoad --- homeassistant/components/pyload/coordinator.py | 1 + tests/components/pyload/snapshots/test_diagnostics.ambr | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index c57dfa7720d..7bb2b870520 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -31,6 +31,7 @@ class PyLoadData: download: bool reconnect: bool captcha: bool | None = None + proxy: bool | None = None free_space: int diff --git a/tests/components/pyload/snapshots/test_diagnostics.ambr b/tests/components/pyload/snapshots/test_diagnostics.ambr index 81a5d750bc0..d773804bf73 100644 --- a/tests/components/pyload/snapshots/test_diagnostics.ambr +++ b/tests/components/pyload/snapshots/test_diagnostics.ambr @@ -13,6 +13,7 @@ 'download': True, 'free_space': 99999999999, 'pause': False, + 'proxy': None, 'queue': 6, 'reconnect': False, 'speed': 5405963.0, From f4c0eb41894355ad57fe5483cca95e4c225b88cd Mon Sep 17 00:00:00 2001 From: Ivan Lopez Hernandez Date: Wed, 26 Mar 2025 22:05:22 -0700 Subject: [PATCH 012/258] Initialize google.genai.Client in the executor (#141432) * Intialize the client on an executor thread * Fix MyPy error * MyPy error * Exception error * Fix ruff * Update __init__.py --------- Co-authored-by: tronikos --- .../google_generative_ai_conversation/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index c32d7b5ddea..88a51446cda 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations import mimetypes from pathlib import Path -from google import genai # type: ignore[attr-defined] +from google.genai import Client from google.genai.errors import APIError, ClientError from requests.exceptions import Timeout import voluptuous as vol @@ -43,7 +43,7 @@ CONF_FILENAMES = "filenames" CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) PLATFORMS = (Platform.CONVERSATION,) -type GoogleGenerativeAIConfigEntry = ConfigEntry[genai.Client] +type GoogleGenerativeAIConfigEntry = ConfigEntry[Client] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -139,7 +139,11 @@ async def async_setup_entry( """Set up Google Generative AI Conversation from a config entry.""" try: - client = genai.Client(api_key=entry.data[CONF_API_KEY]) + + def _init_client() -> Client: + return Client(api_key=entry.data[CONF_API_KEY]) + + client = await hass.async_add_executor_job(_init_client) await client.aio.models.get( model=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), config={"http_options": {"timeout": TIMEOUT_MILLIS}}, From e7ff0a3f8b86560b8efff85f6ea094b2191dabeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 27 Mar 2025 10:18:30 +0100 Subject: [PATCH 013/258] Improve some Home Connect deprecations (#141508) --- .../components/home_connect/binary_sensor.py | 4 +- .../components/home_connect/strings.json | 40 +++++- .../components/home_connect/switch.py | 35 ++++- .../home_connect/test_binary_sensor.py | 81 +++++++++++- tests/components/home_connect/test_switch.py | 124 +++++++++++++++++- tests/components/home_connect/test_time.py | 8 +- 6 files changed, 271 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index b7b7e50047e..a28b4ff2b49 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -244,6 +244,7 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor): BSH_DOOR_STATE_LOCKED: False, BSH_DOOR_STATE_OPEN: True, }, + entity_registry_enabled_default=False, ), ) self._attr_unique_id = f"{appliance.info.ha_id}-Door" @@ -283,7 +284,8 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor): DOMAIN, f"deprecated_binary_common_door_sensor_{self.entity_id}", breaks_in_ha_version="2025.5.0", - is_fixable=False, + is_fixable=True, + is_persistent=True, severity=IssueSeverity.WARNING, translation_key="deprecated_binary_common_door_sensor", translation_placeholders={ diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 44a6eb17cea..5072a4d49a7 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -134,15 +134,47 @@ }, "deprecated_binary_common_door_sensor": { "title": "Deprecated binary door sensor detected in some automations or scripts", - "description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue." + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::home_connect::issues::deprecated_binary_common_door_sensor::title%]", + "description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue." + } + } + } }, "deprecated_command_actions": { "title": "The command related actions are deprecated in favor of the new buttons", - "description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue." + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::home_connect::issues::deprecated_command_actions::title%]", + "description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue." + } + } + } + }, + "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 detected in some automations or scripts", - "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." + "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", diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 33e30f184b7..05f0ed2ddc3 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -266,7 +266,10 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): super().__init__( coordinator, appliance, - SwitchEntityDescription(key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM), + 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}" @@ -304,11 +307,12 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): async_create_issue( self.hass, DOMAIN, - f"deprecated_program_switch_{self.entity_id}", + f"deprecated_program_switch_in_automations_scripts_{self.entity_id}", breaks_in_ha_version="2025.6.0", - is_fixable=False, + is_fixable=True, + is_persistent=True, severity=IssueSeverity.WARNING, - translation_key="deprecated_program_switch", + translation_key="deprecated_program_switch_in_automations_scripts", translation_placeholders={ "entity_id": self.entity_id, "items": "\n".join(items_list), @@ -317,12 +321,34 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): 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 @@ -339,6 +365,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): 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: diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index 31c15ec00cf..ce879a38de5 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -1,6 +1,7 @@ """Tests for home_connect binary_sensor entities.""" from collections.abc import Awaitable, Callable +from http import HTTPStatus from unittest.mock import AsyncMock, MagicMock from aiohomeconnect.model import ( @@ -39,6 +40,7 @@ import homeassistant.helpers.issue_registry as ir from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator @pytest.fixture @@ -165,6 +167,7 @@ async def test_connected_devices( assert len(new_entity_entries) > len(entity_entries) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_binary_sensors_entity_availability( hass: HomeAssistant, @@ -219,6 +222,7 @@ async def test_binary_sensors_entity_availability( assert state.state != STATE_UNAVAILABLE +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) @pytest.mark.parametrize( ("value", "expected"), @@ -402,7 +406,7 @@ async def test_connected_sensor_functionality( @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_create_issue( +async def test_create_door_binary_sensor_deprecation_issue( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -410,7 +414,7 @@ async def test_create_issue( client: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: - """Test we create an issue when an automation or script is using a deprecated entity.""" + """Test that we create an issue when an automation or script is using a door binary sensor entity.""" entity_id = "binary_sensor.washer_door" issue_id = f"deprecated_binary_common_door_sensor_{entity_id}" @@ -464,3 +468,76 @@ 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.usefixtures("entity_registry_enabled_by_default") +async def test_door_binary_sensor_deprecation_issue_fix( + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + setup_credentials: None, + client: MagicMock, + issue_registry: ir.IssueRegistry, + hass_client: ClientSessionGenerator, +) -> None: + """Test that we create an issue when an automation or script is using a door binary sensor entity.""" + entity_id = "binary_sensor.washer_door" + issue_id = f"deprecated_binary_common_door_sensor_{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": [ + { + "condition": "state", + "entity_id": entity_id, + "state": "on", + }, + ], + } + } + }, + ) + + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup(client) + assert config_entry.state == ConfigEntryState.LOADED + + 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 + + _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, issue_id) + assert len(issue_registry.issues) == 0 diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index 2903c8ac718..01f9cad5d2e 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -1,6 +1,7 @@ """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 @@ -59,6 +60,7 @@ from homeassistant.helpers import ( from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator @pytest.fixture @@ -209,6 +211,7 @@ async def test_connected_devices( assert len(new_entity_entries) > len(entity_entries) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", ["Dishwasher"], indirect=True) async def test_switch_entity_availability( hass: HomeAssistant, @@ -320,6 +323,7 @@ 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"), [ @@ -397,6 +401,7 @@ async def test_program_switch_functionality( client.stop_program.assert_awaited_once_with(appliance.ha_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( ( "entity_id", @@ -801,18 +806,24 @@ async def test_power_switch_service_validation_errors( @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_create_issue( +@pytest.mark.parametrize( + "service", + [SERVICE_TURN_ON, SERVICE_TURN_OFF], +) +async def test_create_program_switch_deprecation_issue( hass: HomeAssistant, appliance: HomeAppliance, + service: str, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], setup_credentials: None, client: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: - """Test we create an issue when an automation or script is using a deprecated entity.""" + """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" - issue_id = f"deprecated_program_switch_{entity_id}" + 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, @@ -851,17 +862,118 @@ async def test_create_issue( assert await integration_setup(client) assert config_entry.state == 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) == 1 - assert issue_registry.async_get_issue(DOMAIN, issue_id) + 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, issue_id) + 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, + appliance: HomeAppliance, + service: str, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + setup_credentials: None, + client: MagicMock, + issue_registry: ir.IssueRegistry, + hass_client: ClientSessionGenerator, +) -> 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 config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup(client) + assert config_entry.state == 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 diff --git a/tests/components/home_connect/test_time.py b/tests/components/home_connect/test_time.py index e52e62a8927..8c23a09053a 100644 --- a/tests/components/home_connect/test_time.py +++ b/tests/components/home_connect/test_time.py @@ -320,7 +320,7 @@ async def test_time_entity_error( @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", ["Oven"], indirect=True) -async def test_create_issue( +async def test_create_alarm_clock_deprecation_issue( hass: HomeAssistant, appliance: HomeAppliance, config_entry: MockConfigEntry, @@ -329,7 +329,7 @@ async def test_create_issue( client: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: - """Test we create an issue when an automation or script is using a deprecated entity.""" + """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" automation_script_issue_id = ( f"deprecated_time_alarm_clock_in_automations_scripts_{entity_id}" @@ -401,7 +401,7 @@ async def test_create_issue( @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", ["Oven"], indirect=True) -async def test_issue_fix( +async def test_alarm_clock_deprecation_issue_fix( hass: HomeAssistant, appliance: HomeAppliance, config_entry: MockConfigEntry, @@ -411,7 +411,7 @@ async def test_issue_fix( issue_registry: ir.IssueRegistry, hass_client: ClientSessionGenerator, ) -> None: - """Test we create an issue when an automation or script is using a deprecated entity.""" + """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" automation_script_issue_id = ( f"deprecated_time_alarm_clock_in_automations_scripts_{entity_id}" From ec8363fa4900622606aec6dfda8ed241d9224f8a Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Mar 2025 22:45:28 -0500 Subject: [PATCH 014/258] Add default preannounce sound to Assist satellites (#141522) * Add default preannounce sound * Allow None to disable sound * Register static path instead of HTTP view * Fix path --------- Co-authored-by: Paulus Schoutsen --- .../components/assist_satellite/__init__.py | 17 +++++- .../components/assist_satellite/const.py | 3 + .../components/assist_satellite/entity.py | 10 +-- .../assist_satellite/preannounce.mp3 | Bin 0 -> 17265 bytes .../components/media_player/browse_media.py | 6 +- .../assist_satellite/test_entity.py | 57 +++++++++++++++++- .../esphome/test_assist_satellite.py | 14 ++++- 7 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/assist_satellite/preannounce.mp3 diff --git a/homeassistant/components/assist_satellite/__init__.py b/homeassistant/components/assist_satellite/__init__.py index 31afbda1d11..bc2157b10b2 100644 --- a/homeassistant/components/assist_satellite/__init__.py +++ b/homeassistant/components/assist_satellite/__init__.py @@ -1,9 +1,11 @@ """Base class for assist satellite entities.""" import logging +from pathlib import Path import voluptuous as vol +from homeassistant.components.http import StaticPathConfig from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -15,6 +17,8 @@ from .const import ( CONNECTION_TEST_DATA, DATA_COMPONENT, DOMAIN, + PREANNOUNCE_FILENAME, + PREANNOUNCE_URL, AssistSatelliteEntityFeature, ) from .entity import ( @@ -56,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: { vol.Optional("message"): str, vol.Optional("media_id"): str, - vol.Optional("preannounce_media_id"): str, + vol.Optional("preannounce_media_id"): vol.Any(str, None), } ), cv.has_at_least_one_key("message", "media_id"), @@ -71,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: { vol.Optional("start_message"): str, vol.Optional("start_media_id"): str, - vol.Optional("preannounce_media_id"): str, + vol.Optional("preannounce_media_id"): vol.Any(str, None), vol.Optional("extra_system_prompt"): str, } ), @@ -84,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_register_websocket_api(hass) hass.http.register_view(ConnectionTestView()) + # Default preannounce sound + await hass.http.async_register_static_paths( + [ + StaticPathConfig( + PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME) + ) + ] + ) + return True diff --git a/homeassistant/components/assist_satellite/const.py b/homeassistant/components/assist_satellite/const.py index f7ac7e524b4..7fca88f3b12 100644 --- a/homeassistant/components/assist_satellite/const.py +++ b/homeassistant/components/assist_satellite/const.py @@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey( f"{DOMAIN}_connection_tests" ) +PREANNOUNCE_FILENAME = "preannounce.mp3" +PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}" + class AssistSatelliteEntityFeature(IntFlag): """Supported features of Assist satellite entity.""" diff --git a/homeassistant/components/assist_satellite/entity.py b/homeassistant/components/assist_satellite/entity.py index 450e6cadbc9..7b4c1b92d8c 100644 --- a/homeassistant/components/assist_satellite/entity.py +++ b/homeassistant/components/assist_satellite/entity.py @@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import chat_session, entity from homeassistant.helpers.entity import EntityDescription -from .const import AssistSatelliteEntityFeature +from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature from .errors import AssistSatelliteError, SatelliteBusyError _LOGGER = logging.getLogger(__name__) @@ -180,7 +180,7 @@ class AssistSatelliteEntity(entity.Entity): self, message: str | None = None, media_id: str | None = None, - preannounce_media_id: str | None = None, + preannounce_media_id: str | None = PREANNOUNCE_URL, ) -> None: """Play and show an announcement on the satellite. @@ -190,7 +190,8 @@ class AssistSatelliteEntity(entity.Entity): If media_id is provided, it is played directly. It is possible to omit the message and the satellite will not show any text. - If preannounce_media_id is provided, it is played before the announcement. + If preannounce_media_id is provided, it overrides the default sound. + If preannounce_media_id is None, no sound is played. Calls async_announce with message and media id. """ @@ -228,7 +229,7 @@ class AssistSatelliteEntity(entity.Entity): start_message: str | None = None, start_media_id: str | None = None, extra_system_prompt: str | None = None, - preannounce_media_id: str | None = None, + preannounce_media_id: str | None = PREANNOUNCE_URL, ) -> None: """Start a conversation from the satellite. @@ -239,6 +240,7 @@ class AssistSatelliteEntity(entity.Entity): to omit the message and the satellite will not show any text. If preannounce_media_id is provided, it is played before the announcement. + If preannounce_media_id is None, no sound is played. Calls async_start_conversation. """ diff --git a/homeassistant/components/assist_satellite/preannounce.mp3 b/homeassistant/components/assist_satellite/preannounce.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6e2fa0aba3e22e797c76a5866ea4e9aa6073e860 GIT binary patch literal 17265 zcmd74bx>SQ816Z^yK4w;!6gBLyL)g8?jbmV0RjYfhv2Tk0|bUZAXtFl4k5TZVSqVz z=Brz~wOhBg?mt_*O--GtVW#_c-t(NdpMIx7MP3jE1aC+V`g(c_@F%?Rhq{c00)vQ< zjg$9#246d`_f8(}41zrTe0&V-j^5s$V!XVr)^2t@@9lZq?YueUv@|r}A0mT4?A#qd z+Fl-S?cTq)vt_UeV9@h0(pOi|g#QW+{t)035riN9KW)HOge?dsANf%m#Do+FLPkZ< zFp2{Nj48=|Al!|-Jy;R|6W<079F#(Bvk0C=UAgd`_Yr_#0wDpA$Ge}YV*Y#g;lg>q zxn~gM-$xWF@Snc`067PN=z#Aal1JQunNN@J?mtC=#Mgj4;Of~fobvQ-T_%%NR#1W?z*>w#1hNF*c*s zltucLqlt|ev~3!U>sBatUPT1luCRZJWG)zG27wr)H$h0pJ-h&LQVwL`?Cw`SeE8%2 zF+O`T8UQYiW4UEy5(sf+q=3f)HXOx$659(@x3&7fprrk0W|Ir9snvG}S@bBkv06>~j_8FzX7LIx)yn`v} zS08Cr2MRokR1qC6ew|PzQN&dpNESfewBJQTyavGdtb5rakti7w-<<{{>$sN=?Fjri z^m9n|$ME;Z@SIhxB1A?l)iG zK1#|iPMTwXB&-{lA9ab;byJ+vcrY2pL` z_7(Re;OzI9eJCg~_XOLLpf4~?<2q*lFOh?0WD23te%T=)5RSBMYF(=S$+Q=P<=j3Z z{^Z#2z%EIH2<@e>o8+% znhPsHPD2$Hf(!47O}-nj820RxpeRK9=+ z_8ZC#TLA$236B4Nw)*m^B~-^uFwC9{Y}{r524qwOxMW;&mmI_jA*=~L-fm#P2-QlO zpxWTNX4Orip+6aVke4Ql;HSjF0~_Xs_`Zoll}& zemb1X4R5}qSOpafJd{unb)R}aN9)f5tZ&8UrqjVWWzGqlhJMk}aJ3{OfnKq%ZAoS3 zK6eUetA=e25r1|ZG_YL~A9fS@7IY{ibUDOA&|7M0mlpgs zoAFQwss{^21RMqtXq5Mu(}F(*j^t!L8=`7>J&m+euh&~;pg!9R!W6^!garZ%v!+#M$0RYlq48acI0eCQ3km>3H>V*-vHK6?# z`8niCj^Ga586Rx*k4XIf6FQB-Di{FH>5I&`s^=R+^rrJJ^NGxVVg2G-2uJxUgEWNT ze}+>|5X>uN!%M6RT{mMV+QsK)2t6eXqk#uHV0<;sBtUv}o3Ik|`{JqWXDqw&2ae(A znnYnp9>71A$4|gcUmUOcKO-VZ6l!wJZzalVUsl{&kU>@lb(P#dj=T3|>E?!idU4F| z|9poe{crQ%%h00V?~r6b4DgfxX9P!g;VR|Y04lDH2tf6E!9NfFQo*Cr|4etzD5mC< zuPd=DHnPR?>TRbzal+7dkS1!%!uhzfv%1S&IdvU^p9#w?*e8kG!IT&q`aqwJv+Fop zT+Oct0L$7KuWCGiJt=r}ri@25oSV|qo-)p;jU z>}KO5BjTUs9<+#++6%>TS%bYYD)-{Q=XT(I^U&nz;vX zEdquq9KkP!`778VNr-TU&?R=-!jYyitpip(|Elpf_(`d`ajkdb5d^|V5$}A>ak!iK zR*;=iU`ai0{WHsR)5*#Rz+%16fG1~-)PP+BR7Hf8 z6T*sck!du1Ra4W;lLl%T5=)@J=u(BHFI5*zNPN2R4_^3L#BrdX_!s~Jr0oDKX}J|V z_rzzS1q@XMOkRX<0A-WF##1OVQXk7@8*=|D?fV2Wi#Hz;{N|YVU~4%46+Hg^7Sc>M z`AZgtz6qua|9W}eSmtQm#C?Znd-ye71A`IAr%ZNfvv$b6w^tWRodj@;iIc&p6dfIt zo3wHbLV@7fY*!nIcQ151d?;x@$m{+sk}aPKwD@%^J7SC9HwMD4P`_aEFF}9_@xwjZ z`+7jp4$&)c7;eM~-0+Er_`$D6JcyEDFnE6MMQx7?9%qGMr{A-;x)1=y zLwV5sBI`*X!S9b{9c-7x?VLue3!9{Oq^XzhTaQ=g@=Zvi2vA>mgYzBCt|jlgLzT={}2;H2YaSr3KA}o27TXX34Fy=*iTg50s*d!D}8qJ@&Q1%NDlyV zbK48vt%Nv=@qht#c%mD_V3_%?*RNN?)Hmg=iv0<3b6`d%B7yMH9%p|Gzd(sqnG$n7 zyv{BOcAXf7^V`;9u>|=gakB*wVc+i2za-n8t`_d*FG)X+6PQ0f1c00D@Nwuh1a1`K zqkv;6?A5Q|fvs5UL*>#R@&n^PH_bqQc;)x_)wddO%N|2&4Sj8}D$s3-LOEp0v!~^I zrY#>n{g)uC^=Er5r=jp3;jV`tRUClENdd6Nl1^c`!*_^|$;l}UTP66|s5H($r##no zu*&_3Ad)hsxcQnYPFgTNpqS!#1w0xm2N%c9QAgWcAWzqS{$Uui9IhWByd7d{*bKdq z$t46nz@r7N2Oz->g+~Z5TMRiPe1JPsWGur<-7d@=4lY3*u}ISWYL=#M#*fuL`iB~` zX;o|J3K-oB=8!E~U9Cpow$Zza@dm6`Cu9}u0aVBbBjzN`7;xxP(vXkN(t**o)ABoz z)IR1^6$gE&^!_iJrv<%f+Wp*hSlJr*zR&3m7=Q#)d(79&mH|)|Fq~tyamUKQ1E3nW zyv8FB2Y?xwT!enMFmJ$4N$k#2L|(A25J#5XGr%o4ulpuj{}%WrFZtah>QCU#(A})s z-(HT)1)YYWnSkEi>Nc9SrR`HX9=4K|ZrCXJ2IGT-YBT(vZm|d?@@DpUov1190;0JI%KUgMlyQxtM z-nNIBeG0jSj{_X{e)_bl;sz*-6~Ft{nJx+iW?H-r4@^Uzwirc_w>%DKDx9z$C$i#fPw2Wria zo`-%b2DY8BpN0X>NbZ?PWT_L9n(h#~Ev8M;BG3&!+8ufBxXYh-2-!xji-=rjuIsx~ zr<4r7O30DH3QRq;#_<7wJ#-=Te~i-v*5c)!(IP*DHHSEE7dVQ)0N2N-sgS+Us@MEj zeb@pYvKbgU0-lIkPgmSGPNXxCP%;{n({9{Pd2a7iXsd#_)0C-S^U1}yM8Azt19QX^ zu|0h=w7eqBM*g8aTL_%YotPfpnpQg>>CG4fF2K_W>_lhEQCf*4spF2HQUgA~y>sSC z`?E*%SrDW4vv!NtB;}N1*&j?{2SBrK=8UysYk~%y=6WuEEPFnn584J712>}Zc?ZtF zhV=_w(miW!OUC^U@M(PXj3@@Neo>e*0R-dYCf^g-%1( zkFaC@a*EeaP30(LH$^tHu;DSbfXfH>?9x6SvZy89R-%#K20VE@>05$NIvQ`_gm)%) z-}aTQT-jX!<1k|%tl_l|d$b}D`yk_i_k3KZaYv9ymzXHn`*z<^WTJP}=V{u!t>))1 zv`lnNR-}W_q9RYD;jjCmqHf3aO}p55Z1WkWU6`l~tlZoZZj2 z?M+yyRu$yVIQ;bj4|W2k*U}7FH%pgS!IUv+JWP zF?SE4MuUh&;&&x~F)wF(*0MT`oaYqgNvKQxk-9Z)=Ah%i{DD~lTHW_-E;(o-BgCxt z;R0?-fj2h@{&U=S9|d{!Uu~ytK{G9kb_;?$;6vPS8N5*;XALd#q$Fn6wLA^C?Q`vm z?lSQ)YhUqQ4s!*edAu+B(Ojih6aQf@q#!7{rsZ3fKuo%9F`rypa=$n_fK%M z>gIe<&lN2G;OqzQ_SQBh=vt-*X+XpS=9z+}c-+2=9s#1LV(qG(*FTnu{@sw}$De*- zz7s8oTz&68>C#XhZqFcCa&E?aQ=N9r!YZYZeh*T zOE|x@11A71bSrpJ_OzLbV92?06#C7*Gb4z`Bd^sns%H6bhY&+rg zx>k=<(CNG5o}pO}DVxa~?oFjWKgycDJm6Zmy+=ewdCi+XL$k|V{W$j-e0~dgBK#Sa z84Q7r)}Yyz=F^7?XFu;BU#?(lki5deny_G_GU-byQrjM;)8G;bM{R#8Bb|L+&iDy{og5+fkFiI>_~HD{WZ2MD3qCs=ZxIK?6#(XMPD?I%QoK=P zLz{%+7>^sz9m$aUFZNBpx- zg@I4n*yLvbP>oZuU#{o8wy;qhtOkbrr$C^La1we*?z`O?x&^|DTMqoa<|16^o<(~C z+eXisS zb1q8=Yo<8&R#;G)D%RxHB%LjotHOseX)Q>V7m~6XV!q@(JQO5xWap8L&5dh+m=Z2R zpZY>6Cf_mIQ1$9<<_Fo5aGQ^)M(t@jws;D9Xd&TQz?L(TPA~xgyJRH=WD82zS;+l%JAh%BzjlH52i2cztk2l2Lh;>e zIPoIY{U|jOg(xfXLxHX(a0mZ^w};06^mB&m6UM#4Eo@JG3C*=&U(T&gFNuy0XL!{- zrSPPd^J$Y@BHI0U5+54o1ELVWPi-TooAP#ANTGBH?=k;F_;K!b4y*}r(|Qt={eTu0 z>a@9^fORE^93TbEowR^^uvHFqpDitAtw4?%PuC}n`|_nTQ{Dj5sYuQ+*<=tUMlO@` za0k7mS1peZSkwK)F`vQk!}Z-u^`B#}>NQh3J>L%`gUB79;eAC8zW`w{82^dHLOX!M z?qpiY+7R(S#D>RzoljVkj2h|{Lg%1Nf6(E#FkAL83!FU(uguTQUKLM;|G2d`eDKT$ zks^~B@~CjPFQa||waSm%u^rFr_S)||gK>P{tm^jacW&t)1eVY$z{(@OxZ1C=0noel z_Ij~edl{VB$@(RsX_L(TTj1>0r}1Q#k=3#qwN4uc#b1}7@O;*g{q5qmoR7Xh;)##E zyshAo--4}M+gg_7U)?!e<27w2RNSjn5E8>pHHpovK$-lA_#uM-47V7}I?5m%m3rA5 zYk|2{;M`+s(9kwx;#6VAXUDJsYuKoOm;!--@ywP#8W-v4uKS_fE($F+m2fa*^JB>w zK|E>xsG{hRS{FPaY&HyqZpWZ;c(+|~RqPwzv)J#sMlo^7rFIpY>=aoZkI0-<1dRgU z32EISz(Zl-$>PtMspTt+4OnI+vWjv zKiLC#YG5GTs^wHW4+fj_ z_-<=pi}k6n1auz_J4FbCXB0e6QBhc~lqO}L6HA>15?@J%W!)H27_UZv6ezX|%8-`kTJL(gXb%z%|6< ztYW>*v||PUR!XnHRg$#8JrW;egzF0ir8VvdNp@3flp+H0h~wq-SZ7{r`Ty22L9-Z(ZEae7IKxoYxFxyr(;E0s+{QRUb-Z zVZkx$E1WAxz73iQVHqkRh(A=RgiPz5h|b8*TGuNpmxw2c|r|T5c=WWa$ z2QSRqYG;76Iw2iAhlQuN7lhU?7QlDkPb}z&(#jOy8h@0>r!*~+ktuR*W5V>6JxVY( zMx9A4#B$;V(4~(YgTN2fLx(O?HeK0r=V)fMF&A`>dA02kvkmk&Vr-(r!9` zRyDGI8CK|T>4_`pHUiTsn2QMhA-I2L45SoxCJF2+vA~?|Ix6N#+~9RPKHT?t;T~ob zfPxecI%btNViiFBPA?jtr35^2YS#Lh^u~Ido42__U~A4NN{#;xn1hxZbBLuD9KiLE z?bJn-_%8ueDV+;WS$#QTO$WHuP*o3EpDO;l^snDFd6Y66fhy+cdr1cXkemawywwNH zkK8EXca^2(`LGu33>e`guGsD+wFWINh)jSfSS!`!g~TJ#_^@(bQzz#%QP{{#awGT$ zadLuL1DSm!iA;KfduhqSD`72RA$?sb3X(0)pCG^u_(9j399mZqmuWN+aOe= zMP_#`X2nIaUo6235r>LyfBoWARq}5dJ1pugO%eRRuwpDZR~UpeDTw=xdnsM4#A%YH z(Vm6GkV}H|3CE!OW%M}Fnl;`s*(4k zGA)SpZCPk5kK$1sUTc9ZyiYVPoFiM#Y`F97_^-^QQwS5Bbyzw=vrW&(_CyNhTWHB( zKHa^MM|zeXA< z`Rqg1!LUAg!(fy~=nv1D1^~Wz*9&ZGHm}PQ+n-u;k#R3ya7(@eEcM(L%D9LA0+0n= z`hqdcIZRJ!Ox4bpPlio~%8ktbrqUH&s~FEpXV5<9&7tG&Y&(RtJOEdn=Jf*O;c=`N zkNrCoRa4=7lBaU zjpZS1^c8R~j+^IJ4rIChdR?JvkT{~dNSl~ z_d0iJf_E0fd|J${M(p5H1oJs3G!HMswQQpZJP_1l+s zq@{FakgF^Sm{1{s#PRi%AS>Dg2&wNC@+=k-+K}`x=+m>QTIhNi*?Ogl+sqr^dei^> z=P`CfEj_b@sV2!KJpT~RhRgwmZD8P5%yi+jd)FuUbKvpC{FhfOD^L40YLU?0nJB=N zxnU@v{33EtQp9de%<<>N(CeFN;6eoXq!R$T+UHM!qq*v~HhWtD=zu`#_-VcMs$X;j zMAhDXjYMb7WuRbJ${^t$N{N-2+zy7+kfslZO$%HZ=Z}qFa`$qUv#mKiwpX+gs(*D< z_|6gv!Q8nD-*$e}$7{$-u(wqe(2*>t#V>VB7Aq z-zD}2dOy8hujN-q)_^pVg1kvhMCA3derY(^RGhp~ZCw_-N~>Y2eqGbTy*}(WpEvu5`K5!& z%KNl7H40txu@?*Lc1cf8M8Yf`j;LAMNPH~)eR+Pr4+l}`Q>kT~?c&jQ%|HEN(f~KY z%DxvBQC>u>FT)Y{@(!;jKwM-79}dKNkwGTw8Wb_XMuD-`Sn*UlVt! zajC}4F37wybQ4}*8ILGw?pJ%!?Izv6EG2(d!c{g+cR9If>}EY7v4U=^W0A|>(gd$p zBr-|?58c(7dX1)TokQNujKOCb9P%3_$3s3ItyM17zsG(bZUGi5>8@^8ZUDY(9D;uh zJHeWtmr_WQc)2&zg3>^-CP}ljf;PU9+(-OpqSu5F(h4^oIPkUQ&|R>EkKr=0PN!QP z1`DW~L~W*t{=&&pQlm|{O3`lTnYyN%%B#CKz#--^3Kh%`c{EpH9fn^WHa!=898 zHq7VJ`{q#WJqXr!NG*%XWhsAST}7m@B?umYfAnoYUsu&t@MWafsf^B>Iy-Z&wT=P! ze^j`t#0Uos<3|OvzE(rmx3pu>h{PPfWMFa_ef_Wgu#fYhmYzf6gCT`PP z&oh|#fU&WQQsOr*8btgru&=8b10_r}DYtq|q( zuAx=%)uuMQ2(hC5qLtuc);Nb)FRre|gk?Z7qAUns-)1%pIwLZ=5BV>rp|_v(h}AI^CWR-#y9 zv&AG>YQY9yI-TC4e6KVpuWOLkQ>TUw#Vr%G9Xa{*EjUg|@iJ9in~<&!5-T z%64jUFx_l6Hp0A7i3i~JD4{d&h)N;MX!ODr;w5G0Ji!7 zf5LfKVTG!R6%|Tw-JYQ!SI(LQF)y%*jo2K-B}C@<1NnPIWH%;k)oPdxxR|8 zZ?kcn`RR|GM5H9Qp1UZib3m@2sJJ+OV%~avT=r(o#Ifo?nza?*^v_!vv_uR%Hl@y^ z6%$FGQqPRo|MLG8)_yGmhlG$br9i*4g-~SZpooVxcmF~?E{aC8Yk%quvAIR8n@Ot5 zi}t>qWaM!)MM_YtJrq(Os*;!}R2<2Zo!1kMU92G7)g>n&sowY~fw~?CK*Y^L=-f)$2>}qr!+ZpZ{Y6Wkv4m4C?d)=UtM<8zxvvHQwFsc4&N?cx(P_eME-AP5~ z9*ZN1jSpwvgZ}!kva0~xJv>0{xpB!PH@z1N8>R7YoNkNlrrDgBkxCKqKgA)fW(bh* z<{=g6kAmwbJljlP!6Jv7dZA(Oui^67j^*LCZg-QH-A$8x)xL1g(g$uw7Lh*u4874m zsxfNkH+217B!oK55n;iGM$&x?9;4Al8yYsF!fM+pnUMY`>E+(rc*AxFhMM;1%E~DE zviKH;5g7V7hPo?`vTs#7(;e(T-!hpUo@||mjW#db9{;i#mL|ljZYoP4esQxbli-q{ z{qD>+kITHu{R!q~^J9Rmjl>_na!=9&!GDHxUP zGGoG`A~p2*!tp7(Zrkre{Xqj%&r@2N;IDN_?syCdAd7>?{F~R$F6Zi6N3pQ^PPlu) zu-5Dog8u=#s+yif!drqOXWHF@HZs($!MXNd`E1LV=69XU&3(2BtyFWE(qi{JwXXic zuiR8R?v6?zvck#Bx_Dq~g#EkN zq{p@9GxGJPjPAE{)3itvc*T8P7jwB_`U$?b1J<|}+w!@1Ue{>KLlw_6yVLa&(#CH& z($=pmfibV|m{^l}y!WFS+l%C?7|k2jEgX!BMR{}DMRTTob1CZR-_rg^W%Q7lqEW2^81q{0f(AG!N}G|H748yYe$5hx51 zl+GwGD1Q;Hy8L~E?$V%Xn7pUq^SL`aOO9!^WF8}Bc{H#pSi?A3S(&&-x!3oJ--e#r zqB-(`ZtdE2E8V4g+)-nVchlDO7QugqXH-i~DiNef;NKr>L8)jD+=h;A6b-9gPPi=E zP)a5XPp@1iGej!nq0xp@M#-k5M`USy$;F!T|DE_Pr{$*Fn*`SgvzHj@mXJsNqY0-n z!{7d25z@6{DV||NjDh#FEHSylLO&)1X4b#|S$M7JF80$WNr=DgIVyG^D`xViw}Z}R zb~NH9j>Gox`rS`=k`Be$hJU#sd#Ph%Atq<@R)h(l^S9F|TBa4FPi_V&!==rTC;UB& z!r78{2>yF))M|!c32#X%Cs~D5z5{vSG3w#Qai55z@3R0awjxQZs%3$pmF=NnijK@g z*DIQEVO;})F>A5$pQD{uOFlCyB$0%lLBi!bUFefuw+vJXlJ5CIXeRIC6E~R4GJgq= z$>zItF=ib&y9chywEb(jfIQV8Hr4*Oi59IOShgHAeQ}n`q}j+tBJlmrdyr(J*^wFV z`8e(hA1bT2UC6yoEvz-Hh);MRW_dPXLtzgsB3vlhhagx6!GDS+T}toG=dDRWr7OL! zyqh24YC`dz z5@36;)S7<;NkP9|h~)W*%p>-1*dAd2t7aVK6LKbyk+m_SoXvj|+CPG)EM!_&hA*%} z{TUWzK!G1_$5Xd0oMz=lbWG}YrK;2N0;RLLyY&ZkHogb1@UJ-};YrYECxx#oqza1a z7`utYB4{NCZR5gUn6hZ&UyGTphACk;y7SG{(Xq>vnY0(Z3pS~tsx1gBSLz>3VtSkO`fkoPCF1BQ6MWYFv;dZ# zQ`|=I1K4J@^geL?P*WL7AM)+7Z#J}{DWGci|D2wXidARljLeah4wtF~Gd^i+(9Vfh7Kx)ahGxu}Yd92u-1&$&qXuwcKKmx#^tV)~%c5 zC67v%+yh5os?!Ye1&3meu7RyGdQ(J~QhQB)7WYSaCWRx~-2s(SkrWqizvd$Yzw(9Z z%yMBj7xAUJqxZYj49IR1xa5370+ARgX+llOA=S&($*~Mi)gkkCm*5A>Z=aC4jVTq)yl>*ut+_M3%;oztk#4>s5D z1issV+ehE5iX9EA;94tHarK4Dqs60~y8z}m`oC%R_98{v%&N*FRLzhPIyT}x%l)W* z(75|d;bWoS$|2l761eMfA^0!wzSL5aNz8H*7fTwqQflE&u5SP{^;)pd?ZlsTc3=M{ zxH~mSPQGR;{{3%oIwmE6cDo9b2f6XXMp1nH=XWL{2ZEMQGV!SIirm+k%Co=el$#WD zFtn`tL&H6a-P}49pM68ue|Vu0GbxZ*V_Wl+Nk#NXvEw-r-48S}YxO8Yi~h)~b{GGW z!d|WMVBaA{1~%iI*}NOq+!d|0m7_K#IU}S$oN6mUP{6$72m5r?O?a zbt$`sjrq3SJ`^3ZZc@*y#j_nPFK25QYR`sqm@42i93%M8v6PG9`k$ev;#ihG6dJTG z4C0-n&U|`7#%}7TCt`4+noj=ZZ)6kFpCh-2Z*nlv_LB^x(Anj8v>nXBA+1q5+BaSBwqCfj5*pyH>czcZ zjgCQIn;gF@$r^CIHK-`_;OFJYa2@|nvpZG03<0#~Q3;RFZqwtnpF$-B|1pj_g5O(}P)95ZHz@TtDMq>v^Oj7X;PZ-<+Q{XM@7o#K@U7w$^MLP{ngGN zy(C|gk6lyxou??jab#Wl`_AfBX7EFilx>r5dT)p1aA@z?7fDA+jXn=fFQE$;p;^dt z5Kt2k&@8xc1z$Bnf72Z_jg)fQT{sZYlRL+LGzfbai5x16I!CGFWxGS!6X2}pbfF?b z`j()OeJKl7@a$K5_i-C$m8wb0hEI3}4iR#s#ms;4$L6c1C6sv2Luo82f5>MlUvqck zJwtKS(E6+@8S-G>gx)ksbk=EP!_;asb;(J_Q(}#bQkr_|dh|5!KT;LF=HQ&O7Lc3R zR5tfn^jlhOqeuOpbB#hIXPMHFdGXc+wf%!PTCiBXyI#NnmN18Im&|}M#$Q0L>Z;~- zv~syMY!MK+f=WH7VyfVw%0gy%8uwZla3E!;<#7C+<37P#cmk}oStJ6cu=?AH%71qT zyLHXU3{Dk2Yoz=+o z{&{(B>H;*p7dTwFa}7pX%WwSg?r$H;TWR%T>eTp!6h)_Ja~kym&}r4&-fjf{Kg=)1 z^!|Lh&NN8vGN~dMq1OT*N5MzyVL8_-ww2QEHGAL}jur8yqPX8yZ$lkdjr(Yw%r3&IXuqc7Skwrc1mJC@q_Kj^1^zYq zv;du)>!*0ZZ@u4mleyFqTeR3x#I#3riEgNiFqb+gHy9zXCR&t){UEXGPl|<%+LdJ= z+7f{kk0;gMSRn-eDekvoxPKP%CwjRR1^3VRu1WWBRyCdNZuCWYVG2 zwXfI)N&ReN>0SbNyeFf6d08pfb%K5$j4tc*#502Y=43KM7tl;x3j>@yPmPyo%7r7@wyFNN4)Y!eK?P}&$9@0^~$;u8)ofkA>h~nfoE^;K+4J@{A z-g$Vv`VM1nPWXjgWU51xwh3flVVEV>OsJi)pW_Vn9OOt3OkD*h>0-S2j~{Ei5bmGd zG^um?r7zW$LQnN~@P7kM_w^^*RghEQOk*7uM(LbSe}Fv5YX9^eJ5}qRj6eUoH!`ZN z5ROJ<=}1GZfL~;)_~GA|S%9=6N-TWioH1Z0XL;JFP}I@N5n0W$U?$|su(~?UNo}0|F^3=eROuqq@}l` z*CkfhM<5}neEr$VmMfAXSr4pbiYpok!M}+`Sd7q*CMB^X-2cz!S9#pxDZS*5OaiO% zIPmlc_7tjxoH}#YqDap#WK&l%h3IMEo}@&xaB0SKtE12+nEI+?)!_Qxb8r%^5o^JS zkQ{>u3td1Iv5b?~u3j~zL3O&oc55&MXu7eHB@PQ5uQrLE^H0z*3*j)D))Qi+von-R zEWdu}yw6#AVqJL4Tlp1x&uR4nyaKZFx-X-at`}|P- zkz$@bHAksk7tgH^FQ)yrvx8V)k@ z%zS&IB1HL8CVP3zH! zz`e2G_;eju4}-Wj{5ixFZBm#&cAn@ekKxTH)cgB-z@BvS0Ci-vj+NiztEJ#dfs%H$ zs&XdLte7{fPY%^Wp$@B0Yg(IawKRd$wE5DfzEBr6Rb32cgy}S)3uY61Hk>kG)?|>>WUgxpj z35Z{Tv9SFs{izZSqINQd~Q5sDe(FU-|YFYWFCuzF>JmO3>=%dP3H{Y z=O_;mdX2njU#?5Vbf0q?!B%&zdqHx1PRqIct>#x@oAr-uS9`vf&p1o%EH+5LIf4j9 zid)Q0zdh7sv-c#Z@veuvD%$UaEN#F`@Rh9S`&iO*3QFOb{+|uo`Q%e^Us-Fe2_Lh{ zcG?MGiFi9R38csBX^=jpnARp}%NOX{vb46)C*h0xjYzJEv#QR{HS7j2YX2p$&n!pq zpW#RsGLA}kni2aC#ai$ws#+9X;x^&^BX<-ACyJWn|LEy_H)LGPB-BNe1Cztj{p%ht60s!AxU-!Z z!tJWe;jy_od*rF!ny|dbE&fe|zR+ajnj&grL={zGjtIeji(OSpYb(L$OrskveJQ69 z`bi<<2&mNnW{Xoj-nWQyL2n%IW*!~}vP++3kbRY8e=YG`L2dg-X77aJ3s-T8IkHqr ziN6vjihSm=*TXM8PA=Fn*DtK@H$k-|j2a~ew6<1l1c5ZCw?S#8Pl(kYZ2Lt9tP2Z^ z%W6X;Jw|cxQvo06pJ2!7uJ8$-#;$F60EzG;#J879I3xWQ({yN7g zE2MoVq3uj8BWaKh_s`I!>ejE-CcES{ynG_cnxXLpSfzm>EpoARF$cI!C^9HT48xTM zU5Tql2I`A=M5n?;3onOJe?}IdvNN*Nw8jl8%ofG5;eC?Rl^H6n`9A42pUGHu`R1FB z5|0SSer56NYURLwTg%zS8`LOjMWT|SMn{vxh972+HH++SqUshjn3m*?H@{-8%DPPM z9U7`2FlLY-GH>gc!s1TRC~6t0(gxsO6BHaD{6Fh&Uke#V5dN7Tp8u6+^MMD07jtcI zrOX1fTK_&Aw$I{%lto+EFywwwP5$cQYfjmmT2$U69TJGt+hlv0OUDMYXtc@)Y0)<7 zPP){Sx3Hyp=L}nUZdY%me89FYZ$A0$SvadBt!=22; z;(EJqJdvpZFI6nC(bFSRDS2~&qtJh2GHZsZ0Jmp+t^QCX}dGE0mxkjc0x6ldm;;5v>= zjwxx7ou|G{vGtegj>w{;iG;lr+?YtR{yTA6Y%JMcgh-8CTj~usB=i-(2=<5w0xZn#^9e z`olYgM!ntrU+f^(IMH$ohmgtAPRuD|RRsS74kkiBqde5sUMlH)^XI`oT`r!we2**b zK0h>~iyt&$(>pH{4wU5?KuF9LVQm_WV_q%XjNi&p1kqLP3+U&{ISOf`_wt_AbMMH~ z5lFV0lcnx9?K{1EcZ?mXjb6dwl2uR?PTab7Ju=C%44u0>x_Mj>J)m@Am`haT*wBz;BSHJgXoT5$Q5!S@Vfqc57(~?S z*>S&xty$m8^J=(UnTgctA${~&d`9DDq0@NumfJmD!ui;KC+feyUpT{FErhS%-89LX z#5B?=m8{N6p&1GxC+i;v+FQ>Y+ic~rtKomGph;5n-o}u_LC^MmXQmpNsDAC+wiI7F_9#~ z+mp$~kaYISDBh!;@l9qj34@=BG1jK4Da6sz8b(->l>@ySds!zT8%6SOo(i8VKbxp< z{y%d5FVO#YUP9<+6kWTRl#`i}mhiVC11_cf*Z-Xl{{Q4X`Z7wO|5w}p{r_M5Uk3yJ E2Z%CyM*si- literal 0 HcmV?d00001 diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index c917164a2ee..d234050c1b2 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -23,7 +23,11 @@ from homeassistant.helpers.network import ( from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType # Paths that we don't need to sign -PATHS_WITHOUT_AUTH = ("/api/tts_proxy/", "/api/esphome/ffmpeg_proxy/") +PATHS_WITHOUT_AUTH = ( + "/api/tts_proxy/", + "/api/esphome/ffmpeg_proxy/", + "/api/assist_satellite/static/", +) @callback diff --git a/tests/components/assist_satellite/test_entity.py b/tests/components/assist_satellite/test_entity.py index b9f6da6f96c..2b1cc78943f 100644 --- a/tests/components/assist_satellite/test_entity.py +++ b/tests/components/assist_satellite/test_entity.py @@ -22,6 +22,7 @@ from homeassistant.components.assist_satellite import ( AssistSatelliteAnnouncement, SatelliteBusyError, ) +from homeassistant.components.assist_satellite.const import PREANNOUNCE_URL from homeassistant.components.assist_satellite.entity import AssistSatelliteState from homeassistant.components.media_source import PlayMedia from homeassistant.config_entries import ConfigEntry @@ -185,7 +186,7 @@ async def test_new_pipeline_cancels_pipeline( ("service_data", "expected_params"), [ ( - {"message": "Hello"}, + {"message": "Hello", "preannounce_media_id": None}, AssistSatelliteAnnouncement( message="Hello", media_id="http://10.10.10.10:8123/api/tts_proxy/test-token", @@ -198,6 +199,7 @@ async def test_new_pipeline_cancels_pipeline( { "message": "Hello", "media_id": "media-source://given", + "preannounce_media_id": None, }, AssistSatelliteAnnouncement( message="Hello", @@ -208,7 +210,7 @@ async def test_new_pipeline_cancels_pipeline( ), ), ( - {"media_id": "http://example.com/bla.mp3"}, + {"media_id": "http://example.com/bla.mp3", "preannounce_media_id": None}, AssistSatelliteAnnouncement( message="", media_id="http://example.com/bla.mp3", @@ -368,6 +370,24 @@ async def test_announce_cancels_pipeline( mock_async_announce.assert_called_once() +async def test_announce_default_preannounce( + hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite +) -> None: + """Test announcing on a device with the default preannouncement sound.""" + + async def async_announce(announcement): + assert announcement.preannounce_media_id.endswith(PREANNOUNCE_URL) + + with patch.object(entity, "async_announce", new=async_announce): + await hass.services.async_call( + "assist_satellite", + "announce", + {"media_id": "test-media-id"}, + target={"entity_id": "assist_satellite.test_entity"}, + blocking=True, + ) + + async def test_context_refresh( hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite ) -> None: @@ -521,6 +541,7 @@ async def test_vad_sensitivity_entity_not_found( { "start_message": "Hello", "extra_system_prompt": "Better system prompt", + "preannounce_media_id": None, }, ( "mock-conversation-id", @@ -538,6 +559,7 @@ async def test_vad_sensitivity_entity_not_found( { "start_message": "Hello", "start_media_id": "media-source://given", + "preannounce_media_id": None, }, ( "mock-conversation-id", @@ -552,7 +574,10 @@ async def test_vad_sensitivity_entity_not_found( ), ), ( - {"start_media_id": "http://example.com/given.mp3"}, + { + "start_media_id": "http://example.com/given.mp3", + "preannounce_media_id": None, + }, ( "mock-conversation-id", None, @@ -657,6 +682,32 @@ async def test_start_conversation_reject_builtin_agent( ) +async def test_start_conversation_default_preannounce( + hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite +) -> None: + """Test starting a conversation on a device with the default preannouncement sound.""" + + async def async_start_conversation(start_announcement): + assert PREANNOUNCE_URL in start_announcement.preannounce_media_id + + await async_update_pipeline( + hass, + async_get_pipeline(hass), + conversation_engine="conversation.some_llm", + ) + + with ( + patch.object(entity, "async_start_conversation", new=async_start_conversation), + ): + await hass.services.async_call( + "assist_satellite", + "start_conversation", + {"start_media_id": "test-media-id"}, + target={"entity_id": "assist_satellite.test_entity"}, + blocking=True, + ) + + async def test_wake_word_start_keeps_responding( hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite ) -> None: diff --git a/tests/components/esphome/test_assist_satellite.py b/tests/components/esphome/test_assist_satellite.py index 7fc46e87503..5f433a6c0ed 100644 --- a/tests/components/esphome/test_assist_satellite.py +++ b/tests/components/esphome/test_assist_satellite.py @@ -1249,7 +1249,11 @@ async def test_announce_message( await hass.services.async_call( assist_satellite.DOMAIN, "announce", - {"entity_id": satellite.entity_id, "message": "test-text"}, + { + "entity_id": satellite.entity_id, + "message": "test-text", + "preannounce_media_id": None, + }, blocking=True, ) await done.wait() @@ -1338,6 +1342,7 @@ async def test_announce_media_id( { "entity_id": satellite.entity_id, "media_id": "https://www.home-assistant.io/resolved.mp3", + "preannounce_media_id": None, }, blocking=True, ) @@ -1545,7 +1550,11 @@ async def test_start_conversation_message( await hass.services.async_call( assist_satellite.DOMAIN, "start_conversation", - {"entity_id": satellite.entity_id, "start_message": "test-text"}, + { + "entity_id": satellite.entity_id, + "start_message": "test-text", + "preannounce_media_id": None, + }, blocking=True, ) await done.wait() @@ -1653,6 +1662,7 @@ async def test_start_conversation_media_id( { "entity_id": satellite.entity_id, "start_media_id": "https://www.home-assistant.io/resolved.mp3", + "preannounce_media_id": None, }, blocking=True, ) From 4ead108c1530560799ce0cf1cda4ec5efd89f276 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:22:25 +0100 Subject: [PATCH 015/258] Handle webcal prefix in remote calendar (#141541) Handel webcal prefix in remote calendar --- .../components/remote_calendar/config_flow.py | 4 +++ .../remote_calendar/test_config_flow.py | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/remote_calendar/config_flow.py b/homeassistant/components/remote_calendar/config_flow.py index 03d0e7ea96a..1ceeb7a3937 100644 --- a/homeassistant/components/remote_calendar/config_flow.py +++ b/homeassistant/components/remote_calendar/config_flow.py @@ -42,6 +42,10 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match( {CONF_CALENDAR_NAME: user_input[CONF_CALENDAR_NAME]} ) + if user_input[CONF_URL].startswith("webcal://"): + user_input[CONF_URL] = user_input[CONF_URL].replace( + "webcal://", "https://", 1 + ) self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) client = get_async_client(self.hass) try: diff --git a/tests/components/remote_calendar/test_config_flow.py b/tests/components/remote_calendar/test_config_flow.py index 626bc2c6e03..9eb9cb40134 100644 --- a/tests/components/remote_calendar/test_config_flow.py +++ b/tests/components/remote_calendar/test_config_flow.py @@ -45,6 +45,35 @@ async def test_form_import_ics(hass: HomeAssistant, ics_content: str) -> None: } +@respx.mock +async def test_form_import_webcal(hass: HomeAssistant, ics_content: str) -> None: + """Test we get the import form.""" + respx.get(CALENDER_URL).mock( + return_value=Response( + status_code=200, + text=ics_content, + ) + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_CALENDAR_NAME: CALENDAR_NAME, + CONF_URL: "webcal://some.calendar.com/calendar.ics", + }, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == CALENDAR_NAME + assert result2["data"] == { + CONF_CALENDAR_NAME: CALENDAR_NAME, + CONF_URL: CALENDER_URL, + } + + @pytest.mark.parametrize( ("side_effect"), [ From 2431e1ba98a71a1703166286f90d916a13896089 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 27 Mar 2025 08:51:12 +0100 Subject: [PATCH 016/258] Bump linkplay to v0.2.2 (#141542) Bump linkplay --- 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 0941f2fbe61..02acd0f04f4 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.1"], + "requirements": ["python-linkplay==0.2.2"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index d7db5450a5f..6654c1c30f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2430,7 +2430,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.1 +python-linkplay==0.2.2 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 229c1a76559..bc35748e8ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1967,7 +1967,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.1 +python-linkplay==0.2.2 # homeassistant.components.matter python-matter-server==7.0.0 From dcf2ec5c37c70dd94555be36b24607dfd5f4e8bd Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 27 Mar 2025 10:59:19 +0100 Subject: [PATCH 017/258] Fix sentence-casing in `konnected` strings, replace "override" with "custom" (#141553) Fix sentence-casing in `konnected`strings, replace "Override" with "Custom" Make string consistent with HA standards. As "Override" can be misunderstood as the verb, replace it with "Custom". --- .../components/konnected/strings.json | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json index e1a6863a199..df92e014f12 100644 --- a/homeassistant/components/konnected/strings.json +++ b/homeassistant/components/konnected/strings.json @@ -2,19 +2,19 @@ "config": { "step": { "import_confirm": { - "title": "Import Konnected Device", - "description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry." + "title": "Import Konnected device", + "description": "A Konnected alarm panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry." }, "user": { - "description": "Please enter the host information for your Konnected Panel.", + "description": "Please enter the host information for your Konnected panel.", "data": { "host": "[%key:common::config_flow::data::ip%]", "port": "[%key:common::config_flow::data::port%]" } }, "confirm": { - "title": "Konnected Device Ready", - "description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings." + "title": "Konnected device ready", + "description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected alarm panel settings." } }, "error": { @@ -45,8 +45,8 @@ } }, "options_io_ext": { - "title": "Configure Extended I/O", - "description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.", + "title": "Configure extended I/O", + "description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.", "data": { "8": "Zone 8", "9": "Zone 9", @@ -59,25 +59,25 @@ } }, "options_binary": { - "title": "Configure Binary Sensor", + "title": "Configure binary sensor", "description": "{zone} options", "data": { - "type": "Binary Sensor Type", + "type": "Binary sensor type", "name": "[%key:common::config_flow::data::name%]", "inverse": "Invert the open/close state" } }, "options_digital": { - "title": "Configure Digital Sensor", + "title": "Configure digital sensor", "description": "[%key:component::konnected::options::step::options_binary::description%]", "data": { - "type": "Sensor Type", + "type": "Sensor type", "name": "[%key:common::config_flow::data::name%]", - "poll_interval": "Poll Interval (minutes)" + "poll_interval": "Poll interval (minutes)" } }, "options_switch": { - "title": "Configure Switchable Output", + "title": "Configure switchable output", "description": "{zone} options: state {state}", "data": { "name": "[%key:common::config_flow::data::name%]", @@ -89,18 +89,18 @@ } }, "options_misc": { - "title": "Configure Misc", + "title": "Configure misc", "description": "Please select the desired behavior for your panel", "data": { "discovery": "Respond to discovery requests on your network", "blink": "Blink panel LED on when sending state change", - "override_api_host": "Override default Home Assistant API host panel URL", - "api_host": "Override API host URL" + "override_api_host": "Override default Home Assistant API host URL", + "api_host": "Custom API host URL" } } }, "error": { - "bad_host": "Invalid Override API host URL" + "bad_host": "Invalid custom API host URL" }, "abort": { "not_konn_panel": "[%key:component::konnected::config::abort::not_konn_panel%]" From 5d75e96fbf03bbdbbc042f9b430ae1eeec0df3d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Mar 2025 10:19:35 +0000 Subject: [PATCH 018/258] Bump version to 2025.4.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 98032451838..a06c3844aee 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index 0361152b89c..3716dfeaf8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b1" +version = "2025.4.0b2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 2f139e3cb14edfb67f9bcd250d6ef05fd2482d03 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 27 Mar 2025 16:01:54 +0100 Subject: [PATCH 019/258] Tado fix HomeKit flow (#141525) * Initial commit * Fix * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/tado/config_flow.py | 21 ++++++++++---------- homeassistant/components/tado/strings.json | 4 ++++ tests/components/tado/test_config_flow.py | 21 +++++++++++++------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 64763469885..48c3d30cb2b 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -22,10 +22,7 @@ from homeassistant.config_entries import ( ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.service_info.zeroconf import ( - ATTR_PROPERTIES_ID, - ZeroconfServiceInfo, -) +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .const import ( CONF_FALLBACK, @@ -164,12 +161,16 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN): self, discovery_info: ZeroconfServiceInfo ) -> ConfigFlowResult: """Handle HomeKit discovery.""" - self._async_abort_entries_match() - properties = { - key.lower(): value for key, value in discovery_info.properties.items() - } - await self.async_set_unique_id(properties[ATTR_PROPERTIES_ID]) - self._abort_if_unique_id_configured() + await self._async_handle_discovery_without_unique_id() + return await self.async_step_homekit_confirm() + + async def async_step_homekit_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Prepare for Homekit.""" + if user_input is None: + return self.async_show_form(step_id="homekit_confirm") + return await self.async_step_user() @staticmethod diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index c7aef7eb51c..53de3969998 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -16,6 +16,10 @@ "title": "Authenticate with Tado", "description": "You need to reauthenticate with Tado. Press `Submit` to start the authentication process." }, + "homekit": { + "title": "Authenticate with Tado", + "description": "Your device has been discovered and needs to authenticate with Tado. Press `Submit` to start the authentication process." + }, "timeout": { "description": "The authentication process timed out. Please try again." } diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index f7418309d46..2fd8e6a0468 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -234,13 +234,19 @@ async def test_homekit(hass: HomeAssistant, mock_tado_api: MagicMock) -> None: type="mock_type", ), ) - assert result["type"] is FlowResultType.SHOW_PROGRESS_DONE - flow = next( - flow - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == result["flow_id"] - ) - assert flow["context"]["unique_id"] == "AA:BB:CC:DD:EE:FF" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "homekit_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == "1" + + +async def test_homekit_already_setup( + hass: HomeAssistant, mock_tado_api: MagicMock +) -> None: + """Test that we abort from homekit if tado is already setup.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} @@ -261,3 +267,4 @@ async def test_homekit(hass: HomeAssistant, mock_tado_api: MagicMock) -> None: ), ) assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From 438f226c31d88f3c9b868c973153d83b6be5ff06 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Mar 2025 12:05:45 +0100 Subject: [PATCH 020/258] Add icons to hue effects (#141559) --- homeassistant/components/hue/icons.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/homeassistant/components/hue/icons.json b/homeassistant/components/hue/icons.json index 31464308b0a..646c420f1fe 100644 --- a/homeassistant/components/hue/icons.json +++ b/homeassistant/components/hue/icons.json @@ -1,4 +1,28 @@ { + "entity": { + "light": { + "hue_light": { + "state_attributes": { + "effect": { + "state": { + "candle": "mdi:candle", + "sparkle": "mdi:shimmer", + "glisten": "mdi:creation", + "sunrise": "mdi:weather-sunset-up", + "sunset": "mdi:weather-sunset", + "fire": "mdi:fire", + "prism": "mdi:triangle-outline", + "opal": "mdi:diamond-stone", + "underwater": "mdi:waves", + "cosmos": "mdi:star-shooting", + "sunbeam": "mdi:spotlight-beam", + "enchant": "mdi:magic-staff" + } + } + } + } + } + }, "services": { "hue_activate_scene": { "service": "mdi:palette" From b82e1a9befd182036fc77b8bfff85d018dfe9fc8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 27 Mar 2025 15:29:11 +0100 Subject: [PATCH 021/258] Handle cloud subscription expired for backup upload (#141564) Handle cloud backup subscription expired for upload --- homeassistant/components/cloud/backup.py | 14 ++- tests/components/cloud/test_backup.py | 118 ++++++++++++++++++++++- 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/backup.py b/homeassistant/components/cloud/backup.py index b83c4725663..f4426eabeed 100644 --- a/homeassistant/components/cloud/backup.py +++ b/homeassistant/components/cloud/backup.py @@ -4,13 +4,14 @@ from __future__ import annotations import asyncio from collections.abc import AsyncIterator, Callable, Coroutine, Mapping +from http import HTTPStatus import logging import random from typing import Any -from aiohttp import ClientError +from aiohttp import ClientError, ClientResponseError from hass_nabucasa import Cloud, CloudError -from hass_nabucasa.api import CloudApiNonRetryableError +from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError from hass_nabucasa.cloud_api import ( FilesHandlerListEntry, async_files_delete_file, @@ -120,6 +121,8 @@ class CloudBackupAgent(BackupAgent): """ if not backup.protected: raise BackupAgentError("Cloud backups must be protected") + if self._cloud.subscription_expired: + raise BackupAgentError("Cloud subscription has expired") size = backup.size try: @@ -152,6 +155,13 @@ class CloudBackupAgent(BackupAgent): ) from err raise BackupAgentError(f"Failed to upload backup {err}") from err except CloudError as err: + if ( + isinstance(err, CloudApiError) + and isinstance(err.orig_exc, ClientResponseError) + and err.orig_exc.status == HTTPStatus.FORBIDDEN + and self._cloud.subscription_expired + ): + raise BackupAgentError("Cloud subscription has expired") from err if tries == _RETRY_LIMIT: raise BackupAgentError(f"Failed to upload backup {err}") from err tries += 1 diff --git a/tests/components/cloud/test_backup.py b/tests/components/cloud/test_backup.py index dd6252c4d62..8399e69ab09 100644 --- a/tests/components/cloud/test_backup.py +++ b/tests/components/cloud/test_backup.py @@ -5,9 +5,9 @@ from io import StringIO from typing import Any from unittest.mock import ANY, Mock, PropertyMock, patch -from aiohttp import ClientError +from aiohttp import ClientError, ClientResponseError from hass_nabucasa import CloudError -from hass_nabucasa.api import CloudApiNonRetryableError +from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError from hass_nabucasa.files import FilesError, StorageType import pytest @@ -547,6 +547,120 @@ async def test_agents_upload_not_protected( assert stored_backup["failed_agent_ids"] == ["cloud.cloud"] +@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files") +async def test_agents_upload_not_subscribed( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_storage: dict[str, Any], + cloud: Mock, +) -> None: + """Test upload backup when cloud user is not subscribed.""" + cloud.subscription_expired = True + client = await hass_client() + backup_data = "test" + backup_id = "test-backup" + test_backup = AgentBackup( + addons=[AddonInfo(name="Test", slug="test", version="1.0.0")], + backup_id=backup_id, + database_included=True, + date="1970-01-01T00:00:00.000Z", + extra_metadata={}, + folders=[Folder.MEDIA, Folder.SHARE], + homeassistant_included=True, + homeassistant_version="2024.12.0", + name="Test", + protected=True, + size=len(backup_data), + ) + + with ( + patch( + "homeassistant.components.backup.manager.BackupManager.async_get_backup", + ) as fetch_backup, + patch( + "homeassistant.components.backup.manager.read_backup", + return_value=test_backup, + ), + patch("pathlib.Path.open") as mocked_open, + ): + mocked_open.return_value.read = Mock(side_effect=[backup_data.encode(), b""]) + fetch_backup.return_value = test_backup + resp = await client.post( + "/api/backup/upload?agent_id=cloud.cloud", + data={"file": StringIO(backup_data)}, + ) + await hass.async_block_till_done() + + assert resp.status == 201 + assert cloud.files.upload.call_count == 0 + store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"] + assert len(store_backups) == 1 + stored_backup = store_backups[0] + assert stored_backup["backup_id"] == backup_id + assert stored_backup["failed_agent_ids"] == ["cloud.cloud"] + + +@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files") +async def test_agents_upload_not_subscribed_midway( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_storage: dict[str, Any], + cloud: Mock, +) -> None: + """Test upload backup when cloud subscription expires during the call.""" + client = await hass_client() + backup_data = "test" + backup_id = "test-backup" + test_backup = AgentBackup( + addons=[AddonInfo(name="Test", slug="test", version="1.0.0")], + backup_id=backup_id, + database_included=True, + date="1970-01-01T00:00:00.000Z", + extra_metadata={}, + folders=[Folder.MEDIA, Folder.SHARE], + homeassistant_included=True, + homeassistant_version="2024.12.0", + name="Test", + protected=True, + size=len(backup_data), + ) + + async def mock_upload(*args: Any, **kwargs: Any) -> None: + """Mock file upload.""" + cloud.subscription_expired = True + raise CloudApiError( + "Boom!", orig_exc=ClientResponseError(Mock(), Mock(), status=403) + ) + + cloud.files.upload.side_effect = mock_upload + + with ( + patch( + "homeassistant.components.backup.manager.BackupManager.async_get_backup", + ) as fetch_backup, + patch( + "homeassistant.components.backup.manager.read_backup", + return_value=test_backup, + ), + patch("pathlib.Path.open") as mocked_open, + ): + mocked_open.return_value.read = Mock(side_effect=[backup_data.encode(), b""]) + fetch_backup.return_value = test_backup + resp = await client.post( + "/api/backup/upload?agent_id=cloud.cloud", + data={"file": StringIO(backup_data)}, + ) + await hass.async_block_till_done() + + assert resp.status == 201 + assert cloud.files.upload.call_count == 1 + store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"] + assert len(store_backups) == 1 + stored_backup = store_backups[0] + assert stored_backup["backup_id"] == backup_id + assert stored_backup["failed_agent_ids"] == ["cloud.cloud"] + + @pytest.mark.usefixtures("cloud_logged_in", "mock_list_files") async def test_agents_upload_wrong_size( hass: HomeAssistant, From 028e4f60293e5b66213147b596ef26cb11383b94 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Mar 2025 16:36:45 +0100 Subject: [PATCH 022/258] Also migrate completion time entities in SmartThings (#141572) --- .../components/smartthings/__init__.py | 5 +- tests/components/smartthings/test_init.py | 68 ++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 20325e7d3e5..4f7b8c2ddb9 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -352,7 +352,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return { "new_unique_id": f"{device_id}_{MAIN}_{Capability.THREE_AXIS}_{Attribute.THREE_AXIS}_{new_attribute}", } - if attribute == Attribute.MACHINE_STATE: + if attribute in { + Attribute.MACHINE_STATE, + Attribute.COMPLETION_TIME, + }: capability = determine_machine_type( hass, entry.entry_id, device_id ) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 991f44e4377..1d4b124c60d 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -529,12 +529,28 @@ async def test_entity_unique_id_migration( "microwave_machine_state", "2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_machineState_machineState", ), + ( + "da_ks_microwave_0101x", + SENSOR_DOMAIN, + "2bad3237-4886-e699-1b90-4a51a3d55c8a.ovenJobState", + "2bad3237-4886-e699-1b90-4a51a3d55c8a.completionTime", + "microwave_completion_time", + "2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_completionTime_completionTime", + ), + ( + "da_ks_microwave_0101x", + SENSOR_DOMAIN, + "2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_ovenJobState_ovenJobState", + "2bad3237-4886-e699-1b90-4a51a3d55c8a.completionTime", + "microwave_completion_time", + "2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_completionTime_completionTime", + ), ( "da_wm_dw_000001", SENSOR_DOMAIN, "f36dc7ce-cac0-0667-dc14-a3704eb5e676.dishwasherJobState", "f36dc7ce-cac0-0667-dc14-a3704eb5e676.machineState", - "microwave_machine_state", + "dishwasher_machine_state", "f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_machineState_machineState", ), ( @@ -542,9 +558,25 @@ async def test_entity_unique_id_migration( SENSOR_DOMAIN, "f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_dishwasherJobState_dishwasherJobState", "f36dc7ce-cac0-0667-dc14-a3704eb5e676.machineState", - "microwave_machine_state", + "dishwasher_machine_state", "f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_machineState_machineState", ), + ( + "da_wm_dw_000001", + SENSOR_DOMAIN, + "f36dc7ce-cac0-0667-dc14-a3704eb5e676.dishwasherJobState", + "f36dc7ce-cac0-0667-dc14-a3704eb5e676.completionTime", + "dishwasher_completion_time", + "f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_completionTime_completionTime", + ), + ( + "da_wm_dw_000001", + SENSOR_DOMAIN, + "f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_dishwasherJobState_dishwasherJobState", + "f36dc7ce-cac0-0667-dc14-a3704eb5e676.completionTime", + "dishwasher_completion_time", + "f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_completionTime_completionTime", + ), ( "da_wm_wd_000001", SENSOR_DOMAIN, @@ -561,6 +593,22 @@ async def test_entity_unique_id_migration( "dryer_machine_state", "02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_machineState_machineState", ), + ( + "da_wm_wd_000001", + SENSOR_DOMAIN, + "02f7256e-8353-5bdd-547f-bd5b1647e01b.dryerJobState", + "02f7256e-8353-5bdd-547f-bd5b1647e01b.completionTime", + "dryer_completion_time", + "02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_completionTime_completionTime", + ), + ( + "da_wm_wd_000001", + SENSOR_DOMAIN, + "02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_dryerJobState_dryerJobState", + "02f7256e-8353-5bdd-547f-bd5b1647e01b.completionTime", + "dryer_completion_time", + "02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_completionTime_completionTime", + ), ( "da_wm_wm_000001", SENSOR_DOMAIN, @@ -577,6 +625,22 @@ async def test_entity_unique_id_migration( "washer_machine_state", "f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_machineState_machineState", ), + ( + "da_wm_wm_000001", + SENSOR_DOMAIN, + "f984b91d-f250-9d42-3436-33f09a422a47.washerJobState", + "f984b91d-f250-9d42-3436-33f09a422a47.completionTime", + "washer_completion_time", + "f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_completionTime_completionTime", + ), + ( + "da_wm_wm_000001", + SENSOR_DOMAIN, + "f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_washerJobState_washerJobState", + "f984b91d-f250-9d42-3436-33f09a422a47.completionTime", + "washer_completion_time", + "f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_completionTime_completionTime", + ), ], ) async def test_entity_unique_id_migration_machine_state( From 2567181cc2bcb4d4b63ecf66f007544e5d14a54c Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 27 Mar 2025 11:17:56 -0400 Subject: [PATCH 023/258] Better handle Roborock discovery (#141575) --- homeassistant/components/roborock/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index 1a359faca10..886bebea9b6 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -143,6 +143,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): self, discovery_info: DhcpServiceInfo ) -> ConfigFlowResult: """Handle a flow started by a dhcp discovery.""" + await self._async_handle_discovery_without_unique_id() device_registry = dr.async_get(self.hass) device = device_registry.async_get_device( connections={ From a1fe6b9cf3acca34a604bd3230081075515bf292 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Mar 2025 15:38:31 +0000 Subject: [PATCH 024/258] Bump version to 2025.4.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 a06c3844aee..213bc2c6933 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index 3716dfeaf8c..6215344757b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b2" +version = "2025.4.0b3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 942ed89cc4fe8df55a57e469c877336c1bcbbca4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Mar 2025 17:57:58 +0100 Subject: [PATCH 025/258] Revert "Promote after dependencies in bootstrap" (#141584) Revert "Promote after dependencies in bootstrap (#140352)" This reverts commit 376604096049ac2388a1c9d23c578402acbce0b5. --- homeassistant/bootstrap.py | 28 +++++++++++++++++----------- tests/test_bootstrap.py | 18 ++++++++---------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 962c7871028..02a3b8c8fcc 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -859,14 +859,8 @@ async def _async_set_up_integrations( integrations, all_integrations = await _async_resolve_domains_and_preload( hass, config ) - # Detect all cycles - integrations_after_dependencies = ( - await loader.resolve_integrations_after_dependencies( - hass, all_integrations.values(), set(all_integrations) - ) - ) - all_domains = set(integrations_after_dependencies) - domains = set(integrations) & all_domains + all_domains = set(all_integrations) + domains = set(integrations) _LOGGER.info( "Domains to be set up: %s | %s", @@ -874,8 +868,6 @@ async def _async_set_up_integrations( all_domains - domains, ) - async_set_domains_to_be_loaded(hass, all_domains) - # Initialize recorder if "recorder" in all_domains: recorder.async_initialize_recorder(hass) @@ -908,12 +900,24 @@ async def _async_set_up_integrations( stage_dep_domains_unfiltered = { dep for domain in stage_domains - for dep in integrations_after_dependencies[domain] + for dep in all_integrations[domain].all_dependencies if dep not in stage_domains } stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components stage_all_domains = stage_domains | stage_dep_domains + stage_all_integrations = { + domain: all_integrations[domain] for domain in stage_all_domains + } + # Detect all cycles + stage_integrations_after_dependencies = ( + await loader.resolve_integrations_after_dependencies( + hass, stage_all_integrations.values(), stage_all_domains + ) + ) + stage_all_domains = set(stage_integrations_after_dependencies) + stage_domains &= stage_all_domains + stage_dep_domains &= stage_all_domains _LOGGER.info( "Setting up stage %s: %s | %s\nDependencies: %s | %s", @@ -924,6 +928,8 @@ async def _async_set_up_integrations( stage_dep_domains_unfiltered - stage_dep_domains, ) + async_set_domains_to_be_loaded(hass, stage_all_domains) + if timeout is None: await _async_setup_multi_components(hass, stage_all_domains, config) continue diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index ca75dc51c56..1fb87ac5ef6 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -252,8 +252,8 @@ async def test_setup_after_deps_all_present(hass: HomeAssistant) -> None: @pytest.mark.parametrize("load_registries", [False]) -async def test_setup_after_deps_in_stage_1(hass: HomeAssistant) -> None: - """Test after_dependencies are promoted in stage 1.""" +async def test_setup_after_deps_in_stage_1_ignored(hass: HomeAssistant) -> None: + """Test after_dependencies are ignored in stage 1.""" # This test relies on this assert "cloud" in bootstrap.STAGE_1_INTEGRATIONS order = [] @@ -295,7 +295,7 @@ async def test_setup_after_deps_in_stage_1(hass: HomeAssistant) -> None: assert "normal_integration" in hass.config.components assert "cloud" in hass.config.components - assert order == ["an_after_dep", "normal_integration", "cloud"] + assert order == ["cloud", "an_after_dep", "normal_integration"] @pytest.mark.parametrize("load_registries", [False]) @@ -304,7 +304,7 @@ async def test_setup_after_deps_manifests_are_loaded_even_if_not_setup( ) -> None: """Ensure we preload manifests for after deps even if they are not setup. - It's important that we preload the after dep manifests even if they are not setup + Its important that we preload the after dep manifests even if they are not setup since we will always have to check their requirements since any integration that lists an after dep may import it and we have to ensure requirements are up to date before the after dep can be imported. @@ -371,7 +371,7 @@ async def test_setup_after_deps_manifests_are_loaded_even_if_not_setup( assert "an_after_dep" not in hass.config.components assert "an_after_dep_of_after_dep" not in hass.config.components assert "an_after_dep_of_after_dep_of_after_dep" not in hass.config.components - assert order == ["normal_integration", "cloud"] + assert order == ["cloud", "normal_integration"] assert loader.async_get_loaded_integration(hass, "an_after_dep") is not None assert ( loader.async_get_loaded_integration(hass, "an_after_dep_of_after_dep") @@ -456,9 +456,9 @@ async def test_setup_frontend_before_recorder(hass: HomeAssistant) -> None: assert order == [ "http", - "an_after_dep", "frontend", "recorder", + "an_after_dep", "normal_integration", ] @@ -1577,10 +1577,8 @@ async def test_no_base_platforms_loaded_before_recorder(hass: HomeAssistant) -> assert not isinstance(integrations_or_excs, Exception) integrations[domain] = integration - integrations_all_dependencies = ( - await loader.resolve_integrations_after_dependencies( - hass, integrations.values(), ignore_exceptions=True - ) + integrations_all_dependencies = await loader.resolve_integrations_dependencies( + hass, integrations.values() ) all_integrations = integrations.copy() all_integrations.update( From d8c9655bfd60e98ba57785048a3f80cc3a9b7211 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 27 Mar 2025 17:51:52 +0100 Subject: [PATCH 026/258] Update frontend to 20250327.0 (#141585) --- 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 b78323488ae..ee4db01a6f3 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==20250326.0"] + "requirements": ["home-assistant-frontend==20250327.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d1e91fd8604..bff67afd0d4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.37.0 hass-nabucasa==0.94.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250326.0 +home-assistant-frontend==20250327.0 home-assistant-intents==2025.3.24 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6654c1c30f8..61f2b5158b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250326.0 +home-assistant-frontend==20250327.0 # homeassistant.components.conversation home-assistant-intents==2025.3.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc35748e8ce..af083511dea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250326.0 +home-assistant-frontend==20250327.0 # homeassistant.components.conversation home-assistant-intents==2025.3.24 From 841c7271126a7b1fddcf8ada8111f2ad461bb172 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Mar 2025 16:59:36 +0000 Subject: [PATCH 027/258] Bump version to 2025.4.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 213bc2c6933..db0919d468b 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index 6215344757b..09881066ffe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b3" +version = "2025.4.0b4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From c83a75f6f9ec3eb3cf3385d903712e5e50f229e8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Mar 2025 21:01:52 +0100 Subject: [PATCH 028/258] Add brand for Bosch (#141561) --- homeassistant/brands/bosch.json | 5 +++ homeassistant/generated/integrations.json | 40 +++++++++++++---------- 2 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 homeassistant/brands/bosch.json diff --git a/homeassistant/brands/bosch.json b/homeassistant/brands/bosch.json new file mode 100644 index 00000000000..090cc2af7c3 --- /dev/null +++ b/homeassistant/brands/bosch.json @@ -0,0 +1,5 @@ +{ + "domain": "bosch", + "name": "Bosch", + "integrations": ["bosch_alarm", "bosch_shc", "home_connect"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 58f7f7fab20..f19cd3062a4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -759,17 +759,28 @@ "config_flow": true, "iot_class": "local_push" }, - "bosch_alarm": { - "name": "Bosch Alarm", - "integration_type": "device", - "config_flow": true, - "iot_class": "local_push" - }, - "bosch_shc": { - "name": "Bosch SHC", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_push" + "bosch": { + "name": "Bosch", + "integrations": { + "bosch_alarm": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push", + "name": "Bosch Alarm" + }, + "bosch_shc": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push", + "name": "Bosch SHC" + }, + "home_connect": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_push", + "name": "Home Connect" + } + } }, "brandt": { "name": "Brandt Smart Control", @@ -2639,13 +2650,6 @@ "config_flow": true, "iot_class": "local_polling" }, - "home_connect": { - "name": "Home Connect", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_push", - "single_config_entry": true - }, "home_plus_control": { "name": "Legrand Home+ Control", "integration_type": "virtual", From 22c83e2393e0b2acf79eec6b5def02461cc9adaf Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 27 Mar 2025 18:44:33 +0100 Subject: [PATCH 029/258] Bump aiowebdav2 to 0.4.3 (#141586) --- homeassistant/components/webdav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webdav/manifest.json b/homeassistant/components/webdav/manifest.json index 30028cb28c9..65940eccaf1 100644 --- a/homeassistant/components/webdav/manifest.json +++ b/homeassistant/components/webdav/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aiowebdav2"], "quality_scale": "bronze", - "requirements": ["aiowebdav2==0.4.2"] + "requirements": ["aiowebdav2==0.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 61f2b5158b6..6e39ec720ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webdav -aiowebdav2==0.4.2 +aiowebdav2==0.4.3 # homeassistant.components.webostv aiowebostv==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af083511dea..2cec8d39b8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webdav -aiowebdav2==0.4.2 +aiowebdav2==0.4.3 # homeassistant.components.webostv aiowebostv==0.7.3 From 8d9a4ea2781c917206a76d8fab3bf165f0ff70a5 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 27 Mar 2025 18:39:33 +0100 Subject: [PATCH 030/258] Fix typing error in NMBS (#141589) Fix typing error --- homeassistant/components/nmbs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 822b0236dd0..3552ac3c26d 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -360,7 +360,7 @@ class NMBSSensor(SensorEntity): attrs[ATTR_LONGITUDE] = self.station_coordinates[1] if self.is_via_connection and not self._excl_vias: - via = self._attrs.vias.via[0] + via = self._attrs.vias[0] attrs["via"] = via.station attrs["via_arrival_platform"] = via.arrival.platform From 46c1cbbc9cef7bdfa4feae48c1a113c913dc92a9 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 27 Mar 2025 20:58:59 +0100 Subject: [PATCH 031/258] Update frontend to 20250327.1 (#141596) --- 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 ee4db01a6f3..30bc15ac3bb 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==20250327.0"] + "requirements": ["home-assistant-frontend==20250327.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bff67afd0d4..dcfb34efa07 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.37.0 hass-nabucasa==0.94.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250327.0 +home-assistant-frontend==20250327.1 home-assistant-intents==2025.3.24 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6e39ec720ac..ead44309a15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250327.0 +home-assistant-frontend==20250327.1 # homeassistant.components.conversation home-assistant-intents==2025.3.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cec8d39b8e..e55ab76548d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250327.0 +home-assistant-frontend==20250327.1 # homeassistant.components.conversation home-assistant-intents==2025.3.24 From 4f3e8e9b94960773f55d0dc4728c6d1e2fb252af Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Mar 2025 20:03:14 +0000 Subject: [PATCH 032/258] Bump version to 2025.4.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 db0919d468b..b303b700caa 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index 09881066ffe..9332f1b6f79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b4" +version = "2025.4.0b5" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From d0afabb85c63ec537793a566c8b2fe7ba8c30ba7 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 27 Mar 2025 21:49:16 +0100 Subject: [PATCH 033/258] Fix misleading friendly names of `pvoutput` sensors (#141312) * Fix misleading friendly names of `pvoutput` sensors * Update test_sensor.py * Update test_sensor.py - prettier --- .../components/pvoutput/strings.json | 8 +++--- tests/components/pvoutput/test_sensor.py | 25 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/pvoutput/strings.json b/homeassistant/components/pvoutput/strings.json index 06d98971053..651bb55a2b4 100644 --- a/homeassistant/components/pvoutput/strings.json +++ b/homeassistant/components/pvoutput/strings.json @@ -27,19 +27,19 @@ "entity": { "sensor": { "energy_consumption": { - "name": "Energy consumed" + "name": "Energy consumption" }, "energy_generation": { - "name": "Energy generated" + "name": "Energy generation" }, "efficiency": { "name": "Efficiency" }, "power_consumption": { - "name": "Power consumed" + "name": "Power consumption" }, "power_generation": { - "name": "Power generated" + "name": "Power generation" } } } diff --git a/tests/components/pvoutput/test_sensor.py b/tests/components/pvoutput/test_sensor.py index fbcff94be60..36a37653efe 100644 --- a/tests/components/pvoutput/test_sensor.py +++ b/tests/components/pvoutput/test_sensor.py @@ -30,8 +30,8 @@ async def test_sensors( ) -> None: """Test the PVOutput sensors.""" - state = hass.states.get("sensor.frenck_s_solar_farm_energy_consumed") - entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_consumed") + state = hass.states.get("sensor.frenck_s_solar_farm_energy_consumption") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_consumption") assert entry assert state assert entry.unique_id == "12345_energy_consumption" @@ -40,14 +40,14 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Frenck's Solar Farm Energy consumed" + == "Frenck's Solar Farm Energy consumption" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.frenck_s_solar_farm_energy_generated") - entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_generated") + state = hass.states.get("sensor.frenck_s_solar_farm_energy_generation") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_generation") assert entry assert state assert entry.unique_id == "12345_energy_generation" @@ -56,7 +56,7 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Frenck's Solar Farm Energy generated" + == "Frenck's Solar Farm Energy generation" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR @@ -78,8 +78,8 @@ async def test_sensors( assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.frenck_s_solar_farm_power_consumed") - entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_consumed") + state = hass.states.get("sensor.frenck_s_solar_farm_power_consumption") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_consumption") assert entry assert state assert entry.unique_id == "12345_power_consumption" @@ -87,14 +87,15 @@ async def test_sensors( assert state.state == "2500.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Power consumed" + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Power consumption" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.frenck_s_solar_farm_power_generated") - entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_generated") + state = hass.states.get("sensor.frenck_s_solar_farm_power_generation") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_generation") assert entry assert state assert entry.unique_id == "12345_power_generation" @@ -103,7 +104,7 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Frenck's Solar Farm Power generated" + == "Frenck's Solar Farm Power generation" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT From e795fb9497752abc41b9e95d94e5306cc012d4ff Mon Sep 17 00:00:00 2001 From: alorente Date: Fri, 28 Mar 2025 11:47:41 +0100 Subject: [PATCH 034/258] Fix missing response for queued mode scripts (#141460) --- homeassistant/helpers/script.py | 4 ++-- tests/helpers/test_script.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1242ef3e4d5..43429bdb1d2 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1311,7 +1311,7 @@ class _QueuedScriptRun(_ScriptRun): lock_acquired = False - async def async_run(self) -> None: + async def async_run(self) -> ScriptRunResult | None: """Run script.""" # Wait for previous run, if any, to finish by attempting to acquire the script's # shared lock. At the same time monitor if we've been told to stop. @@ -1325,7 +1325,7 @@ class _QueuedScriptRun(_ScriptRun): self.lock_acquired = True # We've acquired the lock so we can go ahead and start the run. - await super().async_run() + return await super().async_run() def _finish(self) -> None: if self.lock_acquired: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index f8552fcefed..4c707590528 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -5853,14 +5853,16 @@ async def test_stop_action_subscript( ) +@pytest.mark.parametrize(("var", "response"), [(1, "If: Then"), (2, "Testing 123")]) @pytest.mark.parametrize( - ("var", "response"), - [(1, "If: Then"), (2, "Testing 123")], + ("script_mode", "max_runs"), [("single", 1), ("parallel", 2), ("queued", 2)] ) async def test_stop_action_response_variables( hass: HomeAssistant, var: int, response: str, + script_mode, + max_runs, ) -> None: """Test setting stop response_variable in a subscript.""" sequence = cv.SCRIPT_SCHEMA( @@ -5879,7 +5881,14 @@ async def test_stop_action_response_variables( {"stop": "In the name of love", "response_variable": "output"}, ] ) - script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + script_obj = script.Script( + hass, + sequence, + "Test Name", + "test_domain", + script_mode=script_mode, + max_runs=max_runs, + ) run_vars = MappingProxyType({"var": var}) result = await script_obj.async_run(run_vars, context=Context()) From 5c8e415a76d698cd2197b3ae14afa6e71cd57755 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Mar 2025 11:48:34 +0100 Subject: [PATCH 035/258] Add default string and icon for light effect off (#141567) --- homeassistant/components/light/icons.json | 10 +++++++++- homeassistant/components/light/strings.json | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/icons.json b/homeassistant/components/light/icons.json index df98def090e..6218c733f4c 100644 --- a/homeassistant/components/light/icons.json +++ b/homeassistant/components/light/icons.json @@ -1,7 +1,15 @@ { "entity_component": { "_": { - "default": "mdi:lightbulb" + "default": "mdi:lightbulb", + "state_attributes": { + "effect": { + "default": "mdi:circle-medium", + "state": { + "off": "mdi:star-off" + } + } + } } }, "services": { diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index c0f658c3a44..4a3b98ded46 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -93,7 +93,10 @@ "name": "Color temperature (Kelvin)" }, "effect": { - "name": "Effect" + "name": "Effect", + "state": { + "off": "[%key:common::state::off%]" + } }, "effect_list": { "name": "Available effects" From e11409ef99201d481414d26c37536dc9d457ba5c Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 28 Mar 2025 11:40:24 +0100 Subject: [PATCH 036/258] Reverts #141363 "Deprecate SmartThings machine state sensors" (#141573) Reverts #141363 --- homeassistant/components/smartthings/sensor.py | 2 -- homeassistant/components/smartthings/strings.json | 4 ---- tests/components/smartthings/test_sensor.py | 2 -- 3 files changed, 8 deletions(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index f93b27337e1..424483d9617 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -331,7 +331,6 @@ CAPABILITY_TO_SENSORS: dict[ translation_key="dryer_machine_state", options=WASHER_OPTIONS, device_class=SensorDeviceClass.ENUM, - deprecated=lambda _: "machine_state", ) ], Attribute.DRYER_JOB_STATE: [ @@ -966,7 +965,6 @@ CAPABILITY_TO_SENSORS: dict[ translation_key="washer_machine_state", options=WASHER_OPTIONS, device_class=SensorDeviceClass.ENUM, - deprecated=lambda _: "machine_state", ) ], Attribute.WASHER_JOB_STATE: [ diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index e4cf03178fd..dac7b3cf39a 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -487,10 +487,6 @@ "title": "Deprecated refrigerator door binary sensor detected in some automations or scripts", "description": "The refrigerator door binary sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nSeparate entities for cooler and freezer door are available and should be used going forward. Please use them in the above automations or scripts to fix this issue." }, - "deprecated_machine_state": { - "title": "Deprecated machine state sensor detected in some automations or scripts", - "description": "The machine state sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nA select entity is now available for the machine state and should be used going forward. Please use the new select entity in the above automations or scripts to fix this issue." - }, "deprecated_switch_appliance": { "title": "Deprecated switch detected in some automations or scripts", "description": "The switch `{entity}` 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 to fix this issue." diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index cf49d02b910..fe112b3db6b 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -60,8 +60,6 @@ async def test_state_update( @pytest.mark.parametrize( ("device_fixture", "entity_id", "translation_key"), [ - ("da_wm_wm_000001", "sensor.washer_machine_state", "machine_state"), - ("da_wm_wd_000001", "sensor.dryer_machine_state", "machine_state"), ("hw_q80r_soundbar", "sensor.soundbar_volume", "media_player"), ("hw_q80r_soundbar", "sensor.soundbar_media_playback_status", "media_player"), ("hw_q80r_soundbar", "sensor.soundbar_media_input_source", "media_player"), From c490e350bcb4df417cde56054b5b41340dd3f4cb Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 27 Mar 2025 22:51:11 +0100 Subject: [PATCH 037/258] Make names of switch entities in `gree` consistent with docs (#141580) --- homeassistant/components/gree/strings.json | 4 +-- .../gree/snapshots/test_switch.ambr | 16 +++++----- tests/components/gree/test_switch.py | 30 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/gree/strings.json b/homeassistant/components/gree/strings.json index 45911433b92..403cf7d45fc 100644 --- a/homeassistant/components/gree/strings.json +++ b/homeassistant/components/gree/strings.json @@ -16,13 +16,13 @@ "name": "Panel light" }, "quiet": { - "name": "Quiet" + "name": "Quiet mode" }, "fresh_air": { "name": "Fresh air" }, "xfan": { - "name": "XFan" + "name": "Xtra fan" }, "health_mode": { "name": "Health mode" diff --git a/tests/components/gree/snapshots/test_switch.ambr b/tests/components/gree/snapshots/test_switch.ambr index 836641cb2ab..c3fa3ae24c7 100644 --- a/tests/components/gree/snapshots/test_switch.ambr +++ b/tests/components/gree/snapshots/test_switch.ambr @@ -16,10 +16,10 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'switch', - 'friendly_name': 'fake-device-1 Quiet', + 'friendly_name': 'fake-device-1 Quiet mode', }), 'context': , - 'entity_id': 'switch.fake_device_1_quiet', + 'entity_id': 'switch.fake_device_1_quiet_mode', 'last_changed': , 'last_reported': , 'last_updated': , @@ -40,10 +40,10 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'switch', - 'friendly_name': 'fake-device-1 XFan', + 'friendly_name': 'fake-device-1 Xtra fan', }), 'context': , - 'entity_id': 'switch.fake_device_1_xfan', + 'entity_id': 'switch.fake_device_1_xtra_fan', 'last_changed': , 'last_reported': , 'last_updated': , @@ -109,7 +109,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.fake_device_1_quiet', + 'entity_id': 'switch.fake_device_1_quiet_mode', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -121,7 +121,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Quiet', + 'original_name': 'Quiet mode', 'platform': 'gree', 'previous_unique_id': None, 'supported_features': 0, @@ -173,7 +173,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.fake_device_1_xfan', + 'entity_id': 'switch.fake_device_1_xtra_fan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -185,7 +185,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'XFan', + 'original_name': 'Xtra fan', 'platform': 'gree', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py index e9491796bdf..331b6dfa4a6 100644 --- a/tests/components/gree/test_switch.py +++ b/tests/components/gree/test_switch.py @@ -22,11 +22,11 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -ENTITY_ID_LIGHT_PANEL = f"{SWITCH_DOMAIN}.fake_device_1_panel_light" +ENTITY_ID_PANEL_LIGHT = f"{SWITCH_DOMAIN}.fake_device_1_panel_light" ENTITY_ID_HEALTH_MODE = f"{SWITCH_DOMAIN}.fake_device_1_health_mode" -ENTITY_ID_QUIET = f"{SWITCH_DOMAIN}.fake_device_1_quiet" +ENTITY_ID_QUIET_MODE = f"{SWITCH_DOMAIN}.fake_device_1_quiet_mode" ENTITY_ID_FRESH_AIR = f"{SWITCH_DOMAIN}.fake_device_1_fresh_air" -ENTITY_ID_XFAN = f"{SWITCH_DOMAIN}.fake_device_1_xfan" +ENTITY_ID_XTRA_FAN = f"{SWITCH_DOMAIN}.fake_device_1_xtra_fan" async def async_setup_gree(hass: HomeAssistant) -> MockConfigEntry: @@ -54,11 +54,11 @@ async def test_registry_settings( @pytest.mark.parametrize( "entity", [ - ENTITY_ID_LIGHT_PANEL, + ENTITY_ID_PANEL_LIGHT, ENTITY_ID_HEALTH_MODE, - ENTITY_ID_QUIET, + ENTITY_ID_QUIET_MODE, ENTITY_ID_FRESH_AIR, - ENTITY_ID_XFAN, + ENTITY_ID_XTRA_FAN, ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -81,11 +81,11 @@ async def test_send_switch_on(hass: HomeAssistant, entity: str) -> None: @pytest.mark.parametrize( "entity", [ - ENTITY_ID_LIGHT_PANEL, + ENTITY_ID_PANEL_LIGHT, ENTITY_ID_HEALTH_MODE, - ENTITY_ID_QUIET, + ENTITY_ID_QUIET_MODE, ENTITY_ID_FRESH_AIR, - ENTITY_ID_XFAN, + ENTITY_ID_XTRA_FAN, ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -112,11 +112,11 @@ async def test_send_switch_on_device_timeout( @pytest.mark.parametrize( "entity", [ - ENTITY_ID_LIGHT_PANEL, + ENTITY_ID_PANEL_LIGHT, ENTITY_ID_HEALTH_MODE, - ENTITY_ID_QUIET, + ENTITY_ID_QUIET_MODE, ENTITY_ID_FRESH_AIR, - ENTITY_ID_XFAN, + ENTITY_ID_XTRA_FAN, ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -139,11 +139,11 @@ async def test_send_switch_off(hass: HomeAssistant, entity: str) -> None: @pytest.mark.parametrize( "entity", [ - ENTITY_ID_LIGHT_PANEL, + ENTITY_ID_PANEL_LIGHT, ENTITY_ID_HEALTH_MODE, - ENTITY_ID_QUIET, + ENTITY_ID_QUIET_MODE, ENTITY_ID_FRESH_AIR, - ENTITY_ID_XFAN, + ENTITY_ID_XTRA_FAN, ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") From e7c4fdc8bbb5727c778fa606d79fcfaf1ea3fcd6 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 27 Mar 2025 16:56:11 -0400 Subject: [PATCH 038/258] Bump Python-Snoo to 0.6.5 (#141599) * Bump Python-Snoo to 0.6.5 * add to event_types --- homeassistant/components/snoo/event.py | 1 + homeassistant/components/snoo/manifest.json | 2 +- homeassistant/components/snoo/strings.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/snoo/event.py b/homeassistant/components/snoo/event.py index 5932bfd9862..1e50ee46d90 100644 --- a/homeassistant/components/snoo/event.py +++ b/homeassistant/components/snoo/event.py @@ -31,6 +31,7 @@ async def async_setup_entry( "power", "status_requested", "sticky_white_noise_updated", + "config_change", ], ), ) diff --git a/homeassistant/components/snoo/manifest.json b/homeassistant/components/snoo/manifest.json index 4084a7e3e79..839382b2d84 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.4"] + "requirements": ["python-snoo==0.6.5"] } diff --git a/homeassistant/components/snoo/strings.json b/homeassistant/components/snoo/strings.json index f7cf6a4820b..72b0342c7f4 100644 --- a/homeassistant/components/snoo/strings.json +++ b/homeassistant/components/snoo/strings.json @@ -55,7 +55,8 @@ "activity": "Activity press", "power": "Power button pressed", "status_requested": "Status requested", - "sticky_white_noise_updated": "Sleepytime sounds updated" + "sticky_white_noise_updated": "Sleepytime sounds updated", + "config_change": "Config changed" } } } diff --git a/requirements_all.txt b/requirements_all.txt index ead44309a15..2b8198ce74b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2476,7 +2476,7 @@ python-roborock==2.16.1 python-smarttub==0.0.39 # homeassistant.components.snoo -python-snoo==0.6.4 +python-snoo==0.6.5 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e55ab76548d..3bd67eae3b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2007,7 +2007,7 @@ python-roborock==2.16.1 python-smarttub==0.0.39 # homeassistant.components.snoo -python-snoo==0.6.4 +python-snoo==0.6.5 # homeassistant.components.songpal python-songpal==0.16.2 From a0563f06c9de889b22c4048d3dd330f31ba27bf2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Mar 2025 10:45:48 -1000 Subject: [PATCH 039/258] Fix zeroconf logging level not being respected (#141601) Removes an old logging workaround that is no longer needed fixes #141558 --- homeassistant/components/zeroconf/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index e80b6b8cfdb..86f8dbca792 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -145,8 +145,6 @@ def _async_get_instance(hass: HomeAssistant) -> HaAsyncZeroconf: if DOMAIN in hass.data: return cast(HaAsyncZeroconf, hass.data[DOMAIN]) - logging.getLogger("zeroconf").setLevel(logging.NOTSET) - zeroconf = HaZeroconf(**_async_get_zc_args(hass)) aio_zc = HaAsyncZeroconf(zc=zeroconf) From 67f21429e3ab026cdf4bc4f69693004a28889585 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 27 Mar 2025 23:19:04 +0100 Subject: [PATCH 040/258] Bump aiowebdav2 to 0.4.4 (#141615) --- homeassistant/components/webdav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webdav/manifest.json b/homeassistant/components/webdav/manifest.json index 65940eccaf1..260c569b72b 100644 --- a/homeassistant/components/webdav/manifest.json +++ b/homeassistant/components/webdav/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aiowebdav2"], "quality_scale": "bronze", - "requirements": ["aiowebdav2==0.4.3"] + "requirements": ["aiowebdav2==0.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b8198ce74b..6b0fb923343 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webdav -aiowebdav2==0.4.3 +aiowebdav2==0.4.4 # homeassistant.components.webostv aiowebostv==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bd67eae3b7..79b5ab858a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webdav -aiowebdav2==0.4.3 +aiowebdav2==0.4.4 # homeassistant.components.webostv aiowebostv==0.7.3 From e162499267705b2d9bc87ff236354a6c13df3f16 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 27 Mar 2025 18:43:17 -0400 Subject: [PATCH 041/258] Fix an issue with the switch preview in beta (#141617) Fix an issue with the switch preview --- homeassistant/components/template/switch.py | 7 ++-- tests/components/template/test_switch.py | 43 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index b76fc28b83c..fb3aeb1e42a 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -120,7 +120,7 @@ def rewrite_legacy_to_modern_conf( return switches -def rewrite_options_to_moder_conf(option_config: dict[str, dict]) -> dict[str, dict]: +def rewrite_options_to_modern_conf(option_config: dict[str, dict]) -> dict[str, dict]: """Rewrite option configuration to modern configuration.""" option_config = {**option_config} @@ -189,7 +189,7 @@ async def async_setup_entry( """Initialize config entry.""" _options = dict(config_entry.options) _options.pop("template_type") - _options = rewrite_options_to_moder_conf(_options) + _options = rewrite_options_to_modern_conf(_options) validated_config = SWITCH_CONFIG_SCHEMA(_options) async_add_entities([SwitchTemplate(hass, validated_config, config_entry.entry_id)]) @@ -199,7 +199,8 @@ def async_create_preview_switch( hass: HomeAssistant, name: str, config: dict[str, Any] ) -> SwitchTemplate: """Create a preview switch.""" - validated_config = SWITCH_CONFIG_SCHEMA(config | {CONF_NAME: name}) + updated_config = rewrite_options_to_modern_conf(config) + validated_config = SWITCH_CONFIG_SCHEMA(updated_config | {CONF_NAME: name}) return SwitchTemplate(hass, validated_config, None) diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index f0dbe43b51e..d8877851efe 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -8,6 +8,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components import switch, template from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.template.switch import rewrite_legacy_to_modern_conf +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -17,6 +18,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component @@ -29,6 +31,7 @@ from tests.common import ( mock_component, mock_restore_cache, ) +from tests.typing import WebSocketGenerator TEST_OBJECT_ID = "test_template_switch" TEST_ENTITY_ID = f"switch.{TEST_OBJECT_ID}" @@ -279,6 +282,46 @@ async def test_setup_config_entry( assert state == snapshot +@pytest.mark.parametrize("state_key", ["value_template", "state"]) +async def test_flow_preview( + hass: HomeAssistant, + state_key: str, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + result = await hass.config_entries.flow.async_init( + template.DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": SWITCH_DOMAIN}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == SWITCH_DOMAIN + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My template", state_key: "{{ 'on' }}"}, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"]["state"] == "on" + + @pytest.mark.parametrize( ("count", "state_template"), [(1, "{{ states.switch.test_state.state }}")] ) From d9ec7142d7a2122572a10b3d829dc6cfdf8cf7a8 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 28 Mar 2025 08:48:23 +0100 Subject: [PATCH 042/258] Fix ` volatile_organic_compounds_parts` translation string to be referenced for MQTT subentries device class selector (#141618) * Fix ` volatile_organic_compounds_parts` translation string to be referenced for MQTT subentries device class selector * Fix tests --- homeassistant/components/mqtt/strings.json | 2 +- homeassistant/components/sensor/strings.json | 4 ++-- tests/components/awair/test_sensor.py | 2 +- .../matter/snapshots/test_sensor.ambr | 24 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 60339347f2a..2c9e8eede27 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -453,7 +453,7 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index fe6684a9ca4..123c30da72e 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -278,10 +278,10 @@ "name": "Timestamp" }, "volatile_organic_compounds": { - "name": "VOCs" + "name": "Volatile organic compounds" }, "volatile_organic_compounds_parts": { - "name": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]" + "name": "Volatile organic compounds parts" }, "voltage": { "name": "Voltage" diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 8c9cd6e3a24..040deaf8f80 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -127,7 +127,7 @@ async def test_awair_gen1_sensors( assert_expected_properties( hass, entity_registry, - "sensor.living_room_vocs", + "sensor.living_room_volatile_organic_compounds_parts", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_VOC].unique_id_tag}", "366", { diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 9caa84bbf96..cb26f1d8e70 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -686,7 +686,7 @@ 'state': '20.0', }) # --- -# name: test_sensors[air_purifier][sensor.air_purifier_vocs-entry] +# name: test_sensors[air_purifier][sensor.air_purifier_volatile_organic_compounds_parts-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -701,7 +701,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.air_purifier_vocs', + 'entity_id': 'sensor.air_purifier_volatile_organic_compounds_parts', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -713,7 +713,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'VOCs', + 'original_name': 'Volatile organic compounds parts', 'platform': 'matter', 'previous_unique_id': None, 'supported_features': 0, @@ -722,16 +722,16 @@ 'unit_of_measurement': 'ppm', }) # --- -# name: test_sensors[air_purifier][sensor.air_purifier_vocs-state] +# name: test_sensors[air_purifier][sensor.air_purifier_volatile_organic_compounds_parts-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'volatile_organic_compounds_parts', - 'friendly_name': 'Air Purifier VOCs', + 'friendly_name': 'Air Purifier Volatile organic compounds parts', 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.air_purifier_vocs', + 'entity_id': 'sensor.air_purifier_volatile_organic_compounds_parts', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1167,7 +1167,7 @@ 'state': '20.08', }) # --- -# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_vocs-entry] +# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1182,7 +1182,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_vocs', + 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1194,7 +1194,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'VOCs', + 'original_name': 'Volatile organic compounds parts', 'platform': 'matter', 'previous_unique_id': None, 'supported_features': 0, @@ -1203,16 +1203,16 @@ 'unit_of_measurement': 'ppm', }) # --- -# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_vocs-state] +# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'volatile_organic_compounds_parts', - 'friendly_name': 'lightfi-aq1-air-quality-sensor VOCs', + 'friendly_name': 'lightfi-aq1-air-quality-sensor Volatile organic compounds parts', 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_vocs', + 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts', 'last_changed': , 'last_reported': , 'last_updated': , From 45e273897a04d4b4274fdb4f32ed5705706a8ed1 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 28 Mar 2025 13:59:04 +0300 Subject: [PATCH 043/258] Jewish calendar match omer service variables requirement to documentation (#141620) The documentation and the omer schema require a Nusach to be specified, but the YAML misses that requirement --- homeassistant/components/jewish_calendar/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/jewish_calendar/services.yaml b/homeassistant/components/jewish_calendar/services.yaml index b0fa2cfef6c..894fa30fee3 100644 --- a/homeassistant/components/jewish_calendar/services.yaml +++ b/homeassistant/components/jewish_calendar/services.yaml @@ -6,6 +6,7 @@ count_omer: selector: date: nusach: + required: true example: "sfarad" default: "sfarad" selector: From e4d6e20ebd54c5d590211e640d4100d120e890d9 Mon Sep 17 00:00:00 2001 From: Nick Pesce Date: Fri, 28 Mar 2025 06:11:18 -0400 Subject: [PATCH 044/258] Use correct default value for multi press buttons in the Matter integration (#141630) * Respect the min 2 constraint for the switch MultiPressMax attribute * Update test_event.py * Update generic_switch_multi.json * Fix issue and update tests --- homeassistant/components/matter/event.py | 2 +- .../fixtures/nodes/generic_switch_multi.json | 3 +-- .../matter/snapshots/test_event.ambr | 4 ++++ tests/components/matter/test_event.py | 21 +++++++++++++------ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index 6fa775fd1b9..fa7d96ed1ae 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -69,7 +69,7 @@ class MatterEventEntity(MatterEntity, EventEntity): max_presses_supported = self.get_matter_attribute_value( clusters.Switch.Attributes.MultiPressMax ) - max_presses_supported = min(max_presses_supported or 1, 8) + max_presses_supported = min(max_presses_supported or 2, 8) for i in range(max_presses_supported): event_types.append(f"multi_press_{i + 1}") # noqa: PERF401 elif feature_map & SwitchFeature.kMomentarySwitch: diff --git a/tests/components/matter/fixtures/nodes/generic_switch_multi.json b/tests/components/matter/fixtures/nodes/generic_switch_multi.json index 8923198c31e..4055c9dc336 100644 --- a/tests/components/matter/fixtures/nodes/generic_switch_multi.json +++ b/tests/components/matter/fixtures/nodes/generic_switch_multi.json @@ -72,7 +72,6 @@ "1/59/0": 2, "1/59/65533": 1, "1/59/1": 0, - "1/59/2": 2, "1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], "1/59/65532": 30, "1/59/65528": [], @@ -102,7 +101,7 @@ "2/59/0": 2, "2/59/65533": 1, "2/59/1": 0, - "2/59/2": 2, + "2/59/2": 4, "2/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], "2/59/65532": 30, "2/59/65528": [], diff --git a/tests/components/matter/snapshots/test_event.ambr b/tests/components/matter/snapshots/test_event.ambr index b0ddfaed8bf..153f5751f14 100644 --- a/tests/components/matter/snapshots/test_event.ambr +++ b/tests/components/matter/snapshots/test_event.ambr @@ -132,6 +132,8 @@ 'event_types': list([ 'multi_press_1', 'multi_press_2', + 'multi_press_3', + 'multi_press_4', 'long_press', 'long_release', ]), @@ -172,6 +174,8 @@ 'event_types': list([ 'multi_press_1', 'multi_press_2', + 'multi_press_3', + 'multi_press_4', 'long_press', 'long_release', ]), diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py index f3a318c4e8b..651c71a5dce 100644 --- a/tests/components/matter/test_event.py +++ b/tests/components/matter/test_event.py @@ -36,7 +36,7 @@ async def test_generic_switch_node( assert state assert state.state == "unknown" assert state.name == "Mock Generic Switch Button" - # check event_types from featuremap 30 + # check event_types from featuremap 14 (0b1110) assert state.attributes[ATTR_EVENT_TYPES] == [ "initial_press", "short_release", @@ -76,7 +76,7 @@ async def test_generic_switch_multi_node( assert state_button_1.state == "unknown" # name should be 'DeviceName Button (1)' due to the label set to just '1' assert state_button_1.name == "Mock Generic Switch Button (1)" - # check event_types from featuremap 14 + # check event_types from featuremap 30 (0b11110) and MultiPressMax unset (default 2) assert state_button_1.attributes[ATTR_EVENT_TYPES] == [ "multi_press_1", "multi_press_2", @@ -84,11 +84,20 @@ async def test_generic_switch_multi_node( "long_release", ] # check button 2 - state_button_1 = hass.states.get("event.mock_generic_switch_fancy_button") - assert state_button_1 - assert state_button_1.state == "unknown" + state_button_2 = hass.states.get("event.mock_generic_switch_fancy_button") + assert state_button_2 + assert state_button_2.state == "unknown" # name should be 'DeviceName Fancy Button' due to the label set to 'Fancy Button' - assert state_button_1.name == "Mock Generic Switch Fancy Button" + assert state_button_2.name == "Mock Generic Switch Fancy Button" + # check event_types from featuremap 30 (0b11110) and MultiPressMax 4 + assert state_button_2.attributes[ATTR_EVENT_TYPES] == [ + "multi_press_1", + "multi_press_2", + "multi_press_3", + "multi_press_4", + "long_press", + "long_release", + ] # trigger firing a multi press event await trigger_subscription_callback( From 58ab7d350d1699ba5765d05c4049ef68ff47dec4 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 28 Mar 2025 08:37:55 +0100 Subject: [PATCH 045/258] Fix sentence-casing in `airvisual` user strings (#141632) --- homeassistant/components/airvisual/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json index 148b1368a19..7a5f8b1d5c7 100644 --- a/homeassistant/components/airvisual/strings.json +++ b/homeassistant/components/airvisual/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "geography_by_coords": { - "title": "Configure a Geography", + "title": "Configure a geography", "description": "Use the AirVisual cloud API to monitor a latitude/longitude.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", @@ -56,12 +56,12 @@ "sensor": { "pollutant_label": { "state": { - "co": "Carbon Monoxide", - "n2": "Nitrogen Dioxide", + "co": "Carbon monoxide", + "n2": "Nitrogen dioxide", "o3": "Ozone", "p1": "PM10", "p2": "PM2.5", - "s2": "Sulfur Dioxide" + "s2": "Sulfur dioxide" } }, "pollutant_level": { From e9c76ce694e9f5158a06ba1239d7c0b01ca2e444 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 28 Mar 2025 13:55:36 +0100 Subject: [PATCH 046/258] Fix duplicate 'device' term in MQTT translation strings (#141646) * Fix duplicate 'device' from MQTT translation strings * Update homeassistant/components/mqtt/strings.json --- homeassistant/components/mqtt/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 2c9e8eede27..95cef3119b4 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -285,9 +285,9 @@ "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_url": "Invalid URL", "options_not_allowed_with_state_class_or_uom": "The 'Options' setting is not allowed when state class or unit of measurement are used", - "options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class'. If you continue, the existing options will be reset", + "options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class. If you continue, the existing options will be reset", "options_with_enum_device_class": "Configure options for the enumeration sensor", - "uom_required_for_device_class": "The selected device device class requires a unit" + "uom_required_for_device_class": "The selected device class requires a unit" } } }, From 9965f01609da59c40b2b1949d3b32a6a273536a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Mar 2025 09:01:12 -0400 Subject: [PATCH 047/258] Ensure connection test sound has no preannouncement (#141647) --- homeassistant/components/assist_satellite/websocket_api.py | 3 ++- tests/components/assist_satellite/test_websocket_api.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/assist_satellite/websocket_api.py b/homeassistant/components/assist_satellite/websocket_api.py index 4fc1708b866..0a95880706a 100644 --- a/homeassistant/components/assist_satellite/websocket_api.py +++ b/homeassistant/components/assist_satellite/websocket_api.py @@ -198,7 +198,8 @@ async def websocket_test_connection( hass.async_create_background_task( satellite.async_internal_announce( - media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}" + media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}", + preannounce_media_id=None, ), f"assist_satellite_connection_test_{msg['entity_id']}", ) diff --git a/tests/components/assist_satellite/test_websocket_api.py b/tests/components/assist_satellite/test_websocket_api.py index f0a8f02fc50..23eec7e8461 100644 --- a/tests/components/assist_satellite/test_websocket_api.py +++ b/tests/components/assist_satellite/test_websocket_api.py @@ -445,6 +445,7 @@ async def test_connection_test( assert len(entity.announcements) == 1 assert entity.announcements[0].message == "" + assert entity.announcements[0].preannounce_media_id is None announcement_media_id = entity.announcements[0].media_id hass_url = "http://10.10.10.10:8123" assert announcement_media_id.startswith( From cc93152ff009332bf9dfe918ed970796420d762b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Mar 2025 13:55:11 +0100 Subject: [PATCH 048/258] Fix ESPHome event entity staying unavailable (#141650) --- homeassistant/components/esphome/event.py | 10 +++++++++ tests/components/esphome/test_event.py | 25 ++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/event.py b/homeassistant/components/esphome/event.py index 11a5d0cfb33..f4db3844e3d 100644 --- a/homeassistant/components/esphome/event.py +++ b/homeassistant/components/esphome/event.py @@ -33,6 +33,16 @@ class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity): self._trigger_event(self._state.event_type) self.async_write_ha_state() + @callback + def _on_device_update(self) -> None: + """Call when device updates or entry data changes.""" + super()._on_device_update() + if self._entry_data.available: + # Event entities should go available directly + # when the device comes online and not wait + # for the next data push. + self.async_write_ha_state() + async_setup_entry = partial( platform_async_setup_entry, diff --git a/tests/components/esphome/test_event.py b/tests/components/esphome/test_event.py index c17dc4d98a9..d4688e8ab4e 100644 --- a/tests/components/esphome/test_event.py +++ b/tests/components/esphome/test_event.py @@ -4,6 +4,7 @@ from aioesphomeapi import APIClient, Event, EventInfo import pytest from homeassistant.components.event import EventDeviceClass +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant @@ -11,9 +12,9 @@ from homeassistant.core import HomeAssistant async def test_generic_event_entity( hass: HomeAssistant, mock_client: APIClient, - mock_generic_device_entry, + mock_esphome_device, ) -> None: - """Test a generic event entity.""" + """Test a generic event entity and its availability behavior.""" entity_info = [ EventInfo( object_id="myevent", @@ -26,13 +27,31 @@ async def test_generic_event_entity( ] states = [Event(key=1, event_type="type1")] user_service = [] - await mock_generic_device_entry( + device = await mock_esphome_device( mock_client=mock_client, entity_info=entity_info, user_service=user_service, states=states, ) + await hass.async_block_till_done() + + # Test initial state state = hass.states.get("event.test_myevent") assert state is not None assert state.state == "2024-04-24T00:00:00.000+00:00" assert state.attributes["event_type"] == "type1" + + # Test device becomes unavailable + await device.mock_disconnect(True) + await hass.async_block_till_done() + state = hass.states.get("event.test_myevent") + assert state.state == STATE_UNAVAILABLE + + # Test device becomes available again + await device.mock_connect() + await hass.async_block_till_done() + + # Event entity should be available immediately without waiting for data + state = hass.states.get("event.test_myevent") + assert state.state == "2024-04-24T00:00:00.000+00:00" + assert state.attributes["event_type"] == "type1" From ff458c841792a7521d1c37ea802209828a44c1ef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Mar 2025 15:04:34 +0000 Subject: [PATCH 049/258] Bump version to 2025.4.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 b303b700caa..4096453cea2 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index 9332f1b6f79..2d3b2737161 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b5" +version = "2025.4.0b6" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 2af0282725f538907e23b1164cfa8a8486171b1d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Mar 2025 10:07:09 -0400 Subject: [PATCH 050/258] Enable the message box on default for satelitte announcement actions (#141654) --- homeassistant/components/assist_satellite/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/assist_satellite/services.yaml b/homeassistant/components/assist_satellite/services.yaml index fd6a4f23ccc..7d334d6a8db 100644 --- a/homeassistant/components/assist_satellite/services.yaml +++ b/homeassistant/components/assist_satellite/services.yaml @@ -8,6 +8,7 @@ announce: message: required: false example: "Time to wake up!" + default: "" selector: text: media_id: @@ -28,6 +29,7 @@ start_conversation: start_message: required: false example: "You left the lights on in the living room. Turn them off?" + default: "" selector: text: start_media_id: From 0be7db6270c5b6d9141aea4216c7813eb8bff4f7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 28 Mar 2025 16:08:14 +0100 Subject: [PATCH 051/258] Update frontend to 20250328.0 (#141659) --- 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 30bc15ac3bb..884436ad4db 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==20250327.1"] + "requirements": ["home-assistant-frontend==20250328.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dcfb34efa07..ee7ba2926c2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.37.0 hass-nabucasa==0.94.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250327.1 +home-assistant-frontend==20250328.0 home-assistant-intents==2025.3.24 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6b0fb923343..9345023506d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250327.1 +home-assistant-frontend==20250328.0 # homeassistant.components.conversation home-assistant-intents==2025.3.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79b5ab858a0..c5f25f2121d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250327.1 +home-assistant-frontend==20250328.0 # homeassistant.components.conversation home-assistant-intents==2025.3.24 From d1512d46bef25d3c058ad8d0dd1b55293b66d0fb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Mar 2025 16:00:45 +0000 Subject: [PATCH 052/258] Bump version to 2025.4.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 4096453cea2..c603623cb75 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index 2d3b2737161..f04643b025f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b6" +version = "2025.4.0b7" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From fcdaea64dabacdbe8c9f458b8503ed808b134c3b Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 28 Mar 2025 20:54:36 +0100 Subject: [PATCH 053/258] Tado add proper off state (#135480) * Add proper off state * Remove current temp * Add default frost temp --- homeassistant/components/tado/climate.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 6a2067ffff1..e6ae623d1fc 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -477,11 +477,9 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - # If the target temperature will be None - # if the device is performing an action - # that does not affect the temperature or - # the device is switching states - return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp + if self._current_tado_hvac_mode == CONST_MODE_OFF: + return TADO_DEFAULT_MIN_TEMP + return self._tado_zone_data.target_temp async def set_timer( self, From 147bee57e130b0a604c36671b00d024dd6f7fc3a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:00:05 -0400 Subject: [PATCH 054/258] Include ZBT-1 and Yellow in device registry (#141623) * Add the Yellow and ZBT-1 to the device registry * Unload platforms * Fix unit tests * Rename the Yellow update entity to `Radio firmware` * Rename `EmberZNet` to `EmberZNet Zigbee` * Prefix the `sw_version` with the firmware type and clean up * Fix unit tests * Remove unnecessary `always_update=False` from data update coordinator --- .../homeassistant_hardware/coordinator.py | 1 - .../homeassistant_sky_connect/__init__.py | 3 +- .../homeassistant_sky_connect/update.py | 41 ++++++++++++++++--- .../homeassistant_yellow/__init__.py | 1 + .../components/homeassistant_yellow/const.py | 5 ++- .../homeassistant_yellow/strings.json | 7 ++++ .../components/homeassistant_yellow/update.py | 33 ++++++++++++--- .../homeassistant_sky_connect/test_update.py | 8 ++-- .../homeassistant_yellow/test_update.py | 7 +++- 9 files changed, 84 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/coordinator.py b/homeassistant/components/homeassistant_hardware/coordinator.py index 9eb900b13fd..c9a5c891328 100644 --- a/homeassistant/components/homeassistant_hardware/coordinator.py +++ b/homeassistant/components/homeassistant_hardware/coordinator.py @@ -31,7 +31,6 @@ class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]): _LOGGER, name="firmware update coordinator", update_interval=FIRMWARE_REFRESH_INTERVAL, - always_update=False, ) self.hass = hass self.session = session diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index b3af47df61d..e8b8c3bb433 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -15,14 +15,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant SkyConnect config entry.""" - await hass.config_entries.async_forward_entry_setups(entry, ["update"]) - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + await hass.config_entries.async_unload_platforms(entry, ["update"]) return True diff --git a/homeassistant/components/homeassistant_sky_connect/update.py b/homeassistant/components/homeassistant_sky_connect/update.py index 43e3f1ca255..96978eb4562 100644 --- a/homeassistant/components/homeassistant_sky_connect/update.py +++ b/homeassistant/components/homeassistant_sky_connect/update.py @@ -21,11 +21,20 @@ from homeassistant.components.update import UpdateDeviceClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +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.entity_platform import AddConfigEntryEntitiesCallback -from .const import FIRMWARE, FIRMWARE_VERSION, NABU_CASA_FIRMWARE_RELEASES_URL +from .const import ( + DOMAIN, + FIRMWARE, + FIRMWARE_VERSION, + NABU_CASA_FIRMWARE_RELEASES_URL, + PRODUCT, + SERIAL_NUMBER, + HardwareVariant, +) _LOGGER = logging.getLogger(__name__) @@ -42,7 +51,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ fw_type="skyconnect_zigbee_ncp", version_key="ezsp_version", expected_firmware_type=ApplicationType.EZSP, - firmware_name="EmberZNet", + firmware_name="EmberZNet Zigbee", ), ApplicationType.SPINEL: FirmwareUpdateEntityDescription( key="firmware", @@ -130,6 +139,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): """SkyConnect firmware update entity.""" bootloader_reset_type = None + _attr_has_entity_name = True def __init__( self, @@ -141,8 +151,18 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): """Initialize the SkyConnect firmware update entity.""" super().__init__(device, config_entry, update_coordinator, entity_description) - self._attr_unique_id = ( - f"{self._config_entry.data['serial_number']}_{self.entity_description.key}" + variant = HardwareVariant.from_usb_product_name( + self._config_entry.data[PRODUCT] + ) + serial_number = self._config_entry.data[SERIAL_NUMBER] + + self._attr_unique_id = f"{serial_number}_{self.entity_description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_number)}, + name=f"{variant.full_name} ({serial_number[:8]})", + model=variant.full_name, + manufacturer="Nabu Casa", + serial_number=serial_number, ) # Use the cached firmware info if it exists @@ -155,6 +175,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): source="homeassistant_sky_connect", ) + def _update_attributes(self) -> None: + """Recompute the attributes of the entity.""" + super()._update_attributes() + + assert self.device_entry is not None + device_registry = dr.async_get(self.hass) + device_registry.async_update_device( + device_id=self.device_entry.id, + sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}", + ) + @callback def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None: """Handle updated firmware info being pushed by an integration.""" diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index 06f908ab61e..71aa8ef99b7 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -62,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + await hass.config_entries.async_unload_platforms(entry, ["update"]) return True diff --git a/homeassistant/components/homeassistant_yellow/const.py b/homeassistant/components/homeassistant_yellow/const.py index b98b1133d01..b8bf17391f9 100644 --- a/homeassistant/components/homeassistant_yellow/const.py +++ b/homeassistant/components/homeassistant_yellow/const.py @@ -2,8 +2,9 @@ DOMAIN = "homeassistant_yellow" -RADIO_MODEL = "Home Assistant Yellow" -RADIO_MANUFACTURER = "Nabu Casa" +MODEL = "Home Assistant Yellow" +MANUFACTURER = "Nabu Casa" + RADIO_DEVICE = "/dev/ttyAMA1" ZHA_HW_DISCOVERY_DATA = { diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index b089e483899..ddff5fd9b6d 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -149,5 +149,12 @@ "run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]", "uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]" } + }, + "entity": { + "update": { + "firmware": { + "name": "Radio firmware" + } + } } } diff --git a/homeassistant/components/homeassistant_yellow/update.py b/homeassistant/components/homeassistant_yellow/update.py index 88d4f2912d3..71913dc9923 100644 --- a/homeassistant/components/homeassistant_yellow/update.py +++ b/homeassistant/components/homeassistant_yellow/update.py @@ -21,13 +21,17 @@ from homeassistant.components.update import UpdateDeviceClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +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.entity_platform import AddConfigEntryEntitiesCallback from .const import ( + DOMAIN, FIRMWARE, FIRMWARE_VERSION, + MANUFACTURER, + MODEL, NABU_CASA_FIRMWARE_RELEASES_URL, RADIO_DEVICE, ) @@ -39,7 +43,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ ApplicationType | None, FirmwareUpdateEntityDescription ] = { ApplicationType.EZSP: FirmwareUpdateEntityDescription( - key="firmware", + key="radio_firmware", display_precision=0, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, @@ -47,10 +51,10 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ fw_type="yellow_zigbee_ncp", version_key="ezsp_version", expected_firmware_type=ApplicationType.EZSP, - firmware_name="EmberZNet", + firmware_name="EmberZNet Zigbee", ), ApplicationType.SPINEL: FirmwareUpdateEntityDescription( - key="firmware", + key="radio_firmware", display_precision=0, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, @@ -61,7 +65,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ firmware_name="OpenThread RCP", ), None: FirmwareUpdateEntityDescription( - key="firmware", + key="radio_firmware", display_precision=0, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, @@ -135,6 +139,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): """Yellow firmware update entity.""" bootloader_reset_type = "yellow" # Triggers a GPIO reset + _attr_has_entity_name = True def __init__( self, @@ -145,8 +150,13 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): ) -> None: """Initialize the Yellow firmware update entity.""" super().__init__(device, config_entry, update_coordinator, entity_description) - self._attr_unique_id = self.entity_description.key + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, "yellow")}, + name=MODEL, + model=MODEL, + manufacturer=MANUFACTURER, + ) # Use the cached firmware info if it exists if self._config_entry.data[FIRMWARE] is not None: @@ -158,6 +168,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): source="homeassistant_yellow", ) + def _update_attributes(self) -> None: + """Recompute the attributes of the entity.""" + super()._update_attributes() + + assert self.device_entry is not None + device_registry = dr.async_get(self.hass) + device_registry.async_update_device( + device_id=self.device_entry.id, + sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}", + ) + @callback def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None: """Handle updated firmware info being pushed by an integration.""" diff --git a/tests/components/homeassistant_sky_connect/test_update.py b/tests/components/homeassistant_sky_connect/test_update.py index 9fb7528987e..7ad0099785b 100644 --- a/tests/components/homeassistant_sky_connect/test_update.py +++ b/tests/components/homeassistant_sky_connect/test_update.py @@ -14,9 +14,7 @@ from .common import USB_DATA_ZBT1 from tests.common import MockConfigEntry -UPDATE_ENTITY_ID = ( - "update.homeassistant_sky_connect_9e2adbd75b8beb119fe564a0f320645d_firmware" -) +UPDATE_ENTITY_ID = "update.home_assistant_connect_zbt_1_9e2adbd7_firmware" async def test_zbt1_update_entity(hass: HomeAssistant) -> None: @@ -59,8 +57,9 @@ async def test_zbt1_update_entity(hass: HomeAssistant) -> None: await hass.async_block_till_done() state_ezsp = hass.states.get(UPDATE_ENTITY_ID) + assert state_ezsp is not None assert state_ezsp.state == "unknown" - assert state_ezsp.attributes["title"] == "EmberZNet" + assert state_ezsp.attributes["title"] == "EmberZNet Zigbee" assert state_ezsp.attributes["installed_version"] == "7.3.1.0" assert state_ezsp.attributes["latest_version"] is None @@ -80,6 +79,7 @@ async def test_zbt1_update_entity(hass: HomeAssistant) -> None: # After the firmware update, the entity has the new version and the correct state state_spinel = hass.states.get(UPDATE_ENTITY_ID) + assert state_spinel is not None assert state_spinel.state == "unknown" assert state_spinel.attributes["title"] == "OpenThread RCP" assert state_spinel.attributes["installed_version"] == "2.4.4.0" diff --git a/tests/components/homeassistant_yellow/test_update.py b/tests/components/homeassistant_yellow/test_update.py index 269ff2afc49..2ce66b95137 100644 --- a/tests/components/homeassistant_yellow/test_update.py +++ b/tests/components/homeassistant_yellow/test_update.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -UPDATE_ENTITY_ID = "update.homeassistant_yellow_firmware" +UPDATE_ENTITY_ID = "update.home_assistant_yellow_firmware" async def test_yellow_update_entity(hass: HomeAssistant) -> None: @@ -24,6 +24,7 @@ async def test_yellow_update_entity(hass: HomeAssistant) -> None: # Set up the Yellow integration yellow_config_entry = MockConfigEntry( + title="Home Assistant Yellow", domain="homeassistant_yellow", data={ "firmware": "ezsp", @@ -62,8 +63,9 @@ async def test_yellow_update_entity(hass: HomeAssistant) -> None: await hass.async_block_till_done() state_ezsp = hass.states.get(UPDATE_ENTITY_ID) + assert state_ezsp is not None assert state_ezsp.state == "unknown" - assert state_ezsp.attributes["title"] == "EmberZNet" + assert state_ezsp.attributes["title"] == "EmberZNet Zigbee" assert state_ezsp.attributes["installed_version"] == "7.3.1.0" assert state_ezsp.attributes["latest_version"] is None @@ -83,6 +85,7 @@ async def test_yellow_update_entity(hass: HomeAssistant) -> None: # After the firmware update, the entity has the new version and the correct state state_spinel = hass.states.get(UPDATE_ENTITY_ID) + assert state_spinel is not None assert state_spinel.state == "unknown" assert state_spinel.attributes["title"] == "OpenThread RCP" assert state_spinel.attributes["installed_version"] == "2.4.4.0" From e1b4edec50319f9392822889f06195942a34a4fd Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 28 Mar 2025 12:03:42 -0500 Subject: [PATCH 055/258] Bump intents and always prefer more literal text (#141663) --- .../components/conversation/default_agent.py | 19 ++++++++----------- .../components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c30e8bb4a92..bed4b4c0dd6 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -650,7 +650,14 @@ class DefaultAgent(ConversationEntity): if ( (maybe_result is None) # first result - or (num_matched_entities > best_num_matched_entities) + or ( + # More literal text matched + result.text_chunks_matched > maybe_result.text_chunks_matched + ) + or ( + # More entities matched + num_matched_entities > best_num_matched_entities + ) or ( # Fewer unmatched entities (num_matched_entities == best_num_matched_entities) @@ -662,16 +669,6 @@ class DefaultAgent(ConversationEntity): and (num_unmatched_entities == best_num_unmatched_entities) and (num_unmatched_ranges > best_num_unmatched_ranges) ) - or ( - # More literal text matched - (num_matched_entities == best_num_matched_entities) - and (num_unmatched_entities == best_num_unmatched_entities) - and (num_unmatched_ranges == best_num_unmatched_ranges) - and ( - result.text_chunks_matched - > maybe_result.text_chunks_matched - ) - ) or ( # Prefer match failures with entities (result.text_chunks_matched == maybe_result.text_chunks_matched) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index acaa2ef0967..a1281764bd5 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.3.24"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.28"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ee7ba2926c2..8172bfb450d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.94.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250328.0 -home-assistant-intents==2025.3.24 +home-assistant-intents==2025.3.28 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/requirements_all.txt b/requirements_all.txt index 9345023506d..726ff3ee413 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1160,7 +1160,7 @@ holidays==0.69 home-assistant-frontend==20250328.0 # homeassistant.components.conversation -home-assistant-intents==2025.3.24 +home-assistant-intents==2025.3.28 # homeassistant.components.homematicip_cloud homematicip==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5f25f2121d..60d78f36d04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -987,7 +987,7 @@ holidays==0.69 home-assistant-frontend==20250328.0 # homeassistant.components.conversation -home-assistant-intents==2025.3.24 +home-assistant-intents==2025.3.28 # homeassistant.components.homematicip_cloud homematicip==1.1.7 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 21e97ac097b..bfdb61096b6 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.6.10,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.3.24 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.3.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 3bb6373df51fb67ba941dee7e576a691fcf0f493 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 28 Mar 2025 14:33:59 -0400 Subject: [PATCH 056/258] Update Duke Energy package to fix integration (#141669) * Update Duke Energy package to fix integration * fix tests --- homeassistant/components/duke_energy/config_flow.py | 4 ++-- homeassistant/components/duke_energy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/duke_energy/conftest.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/duke_energy/config_flow.py b/homeassistant/components/duke_energy/config_flow.py index e06940b0fba..2ec92ff4c12 100644 --- a/homeassistant/components/duke_energy/config_flow.py +++ b/homeassistant/components/duke_energy/config_flow.py @@ -50,10 +50,10 @@ class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - username = auth["cdp_internal_user_id"].lower() + username = auth["internalUserID"].lower() await self.async_set_unique_id(username) self._abort_if_unique_id_configured() - email = auth["email"].lower() + email = auth["loginEmailAddress"].lower() data = { CONF_EMAIL: email, CONF_USERNAME: username, diff --git a/homeassistant/components/duke_energy/manifest.json b/homeassistant/components/duke_energy/manifest.json index ece18d7ad2a..ad64fdd5cc4 100644 --- a/homeassistant/components/duke_energy/manifest.json +++ b/homeassistant/components/duke_energy/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/duke_energy", "iot_class": "cloud_polling", - "requirements": ["aiodukeenergy==0.2.2"] + "requirements": ["aiodukeenergy==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 726ff3ee413..a439774dd30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -225,7 +225,7 @@ aiodiscover==2.6.1 aiodns==3.2.0 # homeassistant.components.duke_energy -aiodukeenergy==0.2.2 +aiodukeenergy==0.3.0 # homeassistant.components.eafm aioeafm==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60d78f36d04..69d32bf1f33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -213,7 +213,7 @@ aiodiscover==2.6.1 aiodns==3.2.0 # homeassistant.components.duke_energy -aiodukeenergy==0.2.2 +aiodukeenergy==0.3.0 # homeassistant.components.eafm aioeafm==0.1.2 diff --git a/tests/components/duke_energy/conftest.py b/tests/components/duke_energy/conftest.py index f74ef43bf07..f82a2353557 100644 --- a/tests/components/duke_energy/conftest.py +++ b/tests/components/duke_energy/conftest.py @@ -61,8 +61,8 @@ def mock_api() -> Generator[AsyncMock]: ): api = mock_api.return_value api.authenticate.return_value = { - "email": "TEST@EXAMPLE.COM", - "cdp_internal_user_id": "test-username", + "loginEmailAddress": "TEST@EXAMPLE.COM", + "internalUserID": "test-username", } api.get_meters.return_value = {} yield api From b7f29c7358352acaf2b1847b8d5f94f6eb50a630 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:38:16 -0400 Subject: [PATCH 057/258] Handle all firmware types for ZBT-1 and Yellow update entities (#141674) Handle other firmware types --- .../homeassistant_hardware/update.py | 2 +- .../homeassistant_sky_connect/update.py | 35 +++++++++++-- .../components/homeassistant_yellow/update.py | 35 +++++++++++-- .../homeassistant_sky_connect/test_update.py | 46 ++++++++++++++++ .../homeassistant_yellow/test_update.py | 52 +++++++++++++++++++ 5 files changed, 163 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/update.py b/homeassistant/components/homeassistant_hardware/update.py index e835286238f..960facc81f8 100644 --- a/homeassistant/components/homeassistant_hardware/update.py +++ b/homeassistant/components/homeassistant_hardware/update.py @@ -199,7 +199,7 @@ class BaseFirmwareUpdateEntity( # This entity is not currently associated with a device so we must manually # give it a name self._attr_name = f"{self._config_entry.title} Update" - self._attr_title = self.entity_description.firmware_name or "unknown" + self._attr_title = self.entity_description.firmware_name or "Unknown" if ( self._current_firmware_info is None diff --git a/homeassistant/components/homeassistant_sky_connect/update.py b/homeassistant/components/homeassistant_sky_connect/update.py index 96978eb4562..5eaa1e220be 100644 --- a/homeassistant/components/homeassistant_sky_connect/update.py +++ b/homeassistant/components/homeassistant_sky_connect/update.py @@ -64,6 +64,28 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ expected_firmware_type=ApplicationType.SPINEL, firmware_name="OpenThread RCP", ), + ApplicationType.CPC: FirmwareUpdateEntityDescription( + key="firmware", + display_precision=0, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + version_parser=lambda fw: fw, + fw_type="skyconnect_multipan", + version_key="cpc_version", + expected_firmware_type=ApplicationType.CPC, + firmware_name="Multiprotocol", + ), + ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription( + key="firmware", + display_precision=0, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + version_parser=lambda fw: fw, + fw_type=None, # We don't want to update the bootloader + version_key="gecko_bootloader_version", + expected_firmware_type=ApplicationType.GECKO_BOOTLOADER, + firmware_name="Gecko Bootloader", + ), None: FirmwareUpdateEntityDescription( key="firmware", display_precision=0, @@ -86,9 +108,16 @@ def _async_create_update_entity( ) -> FirmwareUpdateEntity: """Create an update entity that handles firmware type changes.""" firmware_type = config_entry.data[FIRMWARE] - entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[ - ApplicationType(firmware_type) if firmware_type is not None else None - ] + + try: + entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[ + ApplicationType(firmware_type) + ] + except (KeyError, ValueError): + _LOGGER.debug( + "Unknown firmware type %r, using default entity description", firmware_type + ) + entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None] entity = FirmwareUpdateEntity( device=config_entry.data["device"], diff --git a/homeassistant/components/homeassistant_yellow/update.py b/homeassistant/components/homeassistant_yellow/update.py index 71913dc9923..94989d5c6b6 100644 --- a/homeassistant/components/homeassistant_yellow/update.py +++ b/homeassistant/components/homeassistant_yellow/update.py @@ -64,6 +64,28 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ expected_firmware_type=ApplicationType.SPINEL, firmware_name="OpenThread RCP", ), + ApplicationType.CPC: FirmwareUpdateEntityDescription( + key="firmware", + display_precision=0, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + version_parser=lambda fw: fw, + fw_type="yellow_multipan", + version_key="cpc_version", + expected_firmware_type=ApplicationType.CPC, + firmware_name="Multiprotocol", + ), + ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription( + key="firmware", + display_precision=0, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + version_parser=lambda fw: fw, + fw_type=None, # We don't want to update the bootloader + version_key="gecko_bootloader_version", + expected_firmware_type=ApplicationType.GECKO_BOOTLOADER, + firmware_name="Gecko Bootloader", + ), None: FirmwareUpdateEntityDescription( key="radio_firmware", display_precision=0, @@ -86,9 +108,16 @@ def _async_create_update_entity( ) -> FirmwareUpdateEntity: """Create an update entity that handles firmware type changes.""" firmware_type = config_entry.data[FIRMWARE] - entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[ - ApplicationType(firmware_type) if firmware_type is not None else None - ] + + try: + entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[ + ApplicationType(firmware_type) + ] + except (KeyError, ValueError): + _LOGGER.debug( + "Unknown firmware type %r, using default entity description", firmware_type + ) + entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None] entity = FirmwareUpdateEntity( device=RADIO_DEVICE, diff --git a/tests/components/homeassistant_sky_connect/test_update.py b/tests/components/homeassistant_sky_connect/test_update.py index 7ad0099785b..b6c7291e0af 100644 --- a/tests/components/homeassistant_sky_connect/test_update.py +++ b/tests/components/homeassistant_sky_connect/test_update.py @@ -1,5 +1,7 @@ """Test SkyConnect firmware update entity.""" +import pytest + from homeassistant.components.homeassistant_hardware.helpers import ( async_notify_firmware_info, ) @@ -84,3 +86,47 @@ async def test_zbt1_update_entity(hass: HomeAssistant) -> None: assert state_spinel.attributes["title"] == "OpenThread RCP" assert state_spinel.attributes["installed_version"] == "2.4.4.0" assert state_spinel.attributes["latest_version"] is None + + +@pytest.mark.parametrize( + ("firmware", "version", "expected"), + [ + ("ezsp", "7.3.1.0 build 0", "EmberZNet Zigbee 7.3.1.0"), + ("spinel", "SL-OPENTHREAD/2.4.4.0_GitHub-7074a43e4", "OpenThread RCP 2.4.4.0"), + ("bootloader", "2.4.2", "Gecko Bootloader 2.4.2"), + ("cpc", "4.3.2", "Multiprotocol 4.3.2"), + ("router", "1.2.3.4", "Unknown 1.2.3.4"), # Not supported but still shown + ], +) +async def test_zbt1_update_entity_state( + hass: HomeAssistant, firmware: str, version: str, expected: str +) -> None: + """Test the ZBT-1 firmware update entity with different firmware types.""" + await async_setup_component(hass, "homeassistant", {}) + + zbt1_config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": firmware, + "firmware_version": version, + "device": USB_DATA_ZBT1.device, + "manufacturer": USB_DATA_ZBT1.manufacturer, + "pid": USB_DATA_ZBT1.pid, + "product": USB_DATA_ZBT1.description, + "serial_number": USB_DATA_ZBT1.serial_number, + "vid": USB_DATA_ZBT1.vid, + }, + version=1, + minor_version=3, + ) + zbt1_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(zbt1_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY_ID) + assert state is not None + assert ( + f"{state.attributes['title']} {state.attributes['installed_version']}" + == expected + ) diff --git a/tests/components/homeassistant_yellow/test_update.py b/tests/components/homeassistant_yellow/test_update.py index 2ce66b95137..2cc7b51836c 100644 --- a/tests/components/homeassistant_yellow/test_update.py +++ b/tests/components/homeassistant_yellow/test_update.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant.components.homeassistant_hardware.helpers import ( async_notify_firmware_info, ) @@ -90,3 +92,53 @@ async def test_yellow_update_entity(hass: HomeAssistant) -> None: assert state_spinel.attributes["title"] == "OpenThread RCP" assert state_spinel.attributes["installed_version"] == "2.4.4.0" assert state_spinel.attributes["latest_version"] is None + + +@pytest.mark.parametrize( + ("firmware", "version", "expected"), + [ + ("ezsp", "7.3.1.0 build 0", "EmberZNet Zigbee 7.3.1.0"), + ("spinel", "SL-OPENTHREAD/2.4.4.0_GitHub-7074a43e4", "OpenThread RCP 2.4.4.0"), + ("bootloader", "2.4.2", "Gecko Bootloader 2.4.2"), + ("cpc", "4.3.2", "Multiprotocol 4.3.2"), + ("router", "1.2.3.4", "Unknown 1.2.3.4"), # Not supported but still shown + ], +) +async def test_yellow_update_entity_state( + hass: HomeAssistant, firmware: str, version: str, expected: str +) -> None: + """Test the Yellow firmware update entity with different firmware types.""" + await async_setup_component(hass, "homeassistant", {}) + + # Set up the Yellow integration + yellow_config_entry = MockConfigEntry( + title="Home Assistant Yellow", + domain="homeassistant_yellow", + data={ + "firmware": firmware, + "firmware_version": version, + "device": RADIO_DEVICE, + }, + version=1, + minor_version=3, + ) + yellow_config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.homeassistant_yellow.is_hassio", return_value=True + ), + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ), + ): + assert await hass.config_entries.async_setup(yellow_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY_ID) + assert state is not None + assert ( + f"{state.attributes['title']} {state.attributes['installed_version']}" + == expected + ) From 2a081abc187f1dfbfac74c40f1e38ef50ebb4f81 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Mar 2025 20:36:15 +0100 Subject: [PATCH 058/258] Fix camera proxy with sole image quality settings (#141676) --- homeassistant/components/proxy/camera.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index f6e909f13d1..47fa9454deb 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -104,6 +104,15 @@ def _resize_image(image, opts): new_width = opts.max_width (old_width, old_height) = img.size old_size = len(image) + + # If no max_width specified, only apply quality changes if requested + if new_width is None: + if opts.quality is None: + return image + imgbuf = io.BytesIO() + img.save(imgbuf, "JPEG", optimize=True, quality=quality) + return imgbuf.getvalue() + if old_width <= new_width: if opts.quality is None: _LOGGER.debug("Image is smaller-than/equal-to requested width") From 35717e82167209d228d2cd632e36e270b52cb1b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Mar 2025 09:32:00 -1000 Subject: [PATCH 059/258] Increase websocket_api allowed peak time to 10s (#141680) * Increase websocket_api allowed peak time to 10s fixes #141624 During integration reload or startup, we can end up sending a message for each entity being created for integrations that create them from an external source (ie MQTT) because the messages come in one at a time. This can overload the loop and/or client for more than 5s. While we have done significant work to optimize for this path, we are at the limit at what we can expect clients to be able to process in the time window, so increase the time window. * adjust test --- homeassistant/components/websocket_api/const.py | 2 +- tests/components/websocket_api/test_http.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index a0d031834ae..fce85339430 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -21,7 +21,7 @@ type AsyncWebSocketCommandHandler = Callable[ DOMAIN: Final = "websocket_api" URL: Final = "/api/websocket" PENDING_MSG_PEAK: Final = 1024 -PENDING_MSG_PEAK_TIME: Final = 5 +PENDING_MSG_PEAK_TIME: Final = 10 # Maximum number of messages that can be pending at any given time. # This is effectively the upper limit of the number of entities # that can fire state changes within ~1 second. diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index 03e30c11ee9..370aab1067a 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -241,7 +241,7 @@ async def test_pending_msg_peak( instance: http.WebSocketHandler = cast(http.WebSocketHandler, setup_instance) # Fill the queue past the allowed peak - for _ in range(10): + for _ in range(20): instance._send_message({"overload": "message"}) async_fire_time_changed( @@ -251,7 +251,7 @@ async def test_pending_msg_peak( msg = await websocket_client.receive() assert msg.type is WSMsgType.CLOSE assert "Client unable to keep up with pending messages" in caplog.text - assert "Stayed over 5 for 5 seconds" in caplog.text + assert "Stayed over 5 for 10 seconds" in caplog.text assert "overload" in caplog.text From 049aaa7e8b890bb7eb1408359a5c9f4bd2b28f7e Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 28 Mar 2025 21:22:53 +0100 Subject: [PATCH 060/258] Fix grammar / sentence-casing in `workday` (#141682) * Fix grammar / sentence-casing in `workday` Also replace "country" with common string. * Add two more references * Fix second data description reference * Add "given" to action description for better translations --- homeassistant/components/workday/strings.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index 87fa294dbba..feedc52331b 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -2,13 +2,13 @@ "title": "Workday", "config": { "abort": { - "already_configured": "Workday has already been setup with chosen configuration" + "already_configured": "Workday has already been set up with chosen configuration" }, "step": { "user": { "data": { "name": "[%key:common::config_flow::data::name%]", - "country": "Country" + "country": "[%key:common::config_flow::data::country%]" } }, "options": { @@ -18,7 +18,7 @@ "days_offset": "Offset", "workdays": "Days to include", "add_holidays": "Add holidays", - "remove_holidays": "Remove Holidays", + "remove_holidays": "Remove holidays", "province": "Subdivision of country", "language": "Language for named holidays", "category": "Additional category as holiday" @@ -116,14 +116,14 @@ }, "issues": { "bad_country": { - "title": "Configured Country for {title} does not exist", + "title": "Configured country for {title} does not exist", "fix_flow": { "step": { "country": { "title": "Select country for {title}", "description": "Select a country to use for your Workday sensor.", "data": { - "country": "[%key:component::workday::config::step::user::data::country%]" + "country": "[%key:common::config_flow::data::country%]" } }, "province": { @@ -133,7 +133,7 @@ "province": "[%key:component::workday::config::step::options::data::province%]" }, "data_description": { - "province": "State, Territory, Province, Region of Country" + "province": "[%key:component::workday::config::step::options::data_description::province%]" } } } @@ -150,7 +150,7 @@ "province": "[%key:component::workday::config::step::options::data::province%]" }, "data_description": { - "province": "[%key:component::workday::issues::bad_country::fix_flow::step::province::data_description::province%]" + "province": "[%key:component::workday::config::step::options::data_description::province%]" } } } @@ -217,7 +217,7 @@ "services": { "check_date": { "name": "Check date", - "description": "Check if date is workday.", + "description": "Checks if a given date is a workday.", "fields": { "check_date": { "name": "Date", From 14eed1778b36d0d9e79ec066400677e22efa48b5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Mar 2025 20:46:26 +0000 Subject: [PATCH 061/258] Bump version to 2025.4.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 c603623cb75..4720dbea545 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index f04643b025f..79d203b61ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b7" +version = "2025.4.0b8" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From d9720283df6f31c687f73f45cf417c67908c354a Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 29 Mar 2025 18:38:19 +0100 Subject: [PATCH 062/258] Add unkown to uncalibrated state for tedee (#141262) --- homeassistant/components/tedee/binary_sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tedee/binary_sensor.py b/homeassistant/components/tedee/binary_sensor.py index a01b889ef8f..6570d9c5428 100644 --- a/homeassistant/components/tedee/binary_sensor.py +++ b/homeassistant/components/tedee/binary_sensor.py @@ -41,7 +41,7 @@ ENTITIES: tuple[TedeeBinarySensorEntityDescription, ...] = ( TedeeBinarySensorEntityDescription( key="semi_locked", translation_key="semi_locked", - is_on_fn=lambda lock: lock.state == TedeeLockState.HALF_OPEN, + is_on_fn=lambda lock: lock.state is TedeeLockState.HALF_OPEN, entity_category=EntityCategory.DIAGNOSTIC, ), TedeeBinarySensorEntityDescription( @@ -53,7 +53,10 @@ ENTITIES: tuple[TedeeBinarySensorEntityDescription, ...] = ( TedeeBinarySensorEntityDescription( key="uncalibrated", translation_key="uncalibrated", - is_on_fn=lambda lock: lock.state == TedeeLockState.UNCALIBRATED, + is_on_fn=( + lambda lock: lock.state is TedeeLockState.UNCALIBRATED + or lock.state is TedeeLockState.UNKNOWN + ), device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, From fad75a70b62736d3c1c67cf90098be4ce7b76697 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 28 Mar 2025 15:52:44 +0100 Subject: [PATCH 063/258] Add a common string for "country" (#141653) --- homeassistant/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 29b7db7a011..dd3caa1ff51 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -47,6 +47,7 @@ "access_token": "Access token", "api_key": "API key", "api_token": "API token", + "country": "Country", "device": "Device", "elevation": "Elevation", "email": "Email", From 3e0eb5ab2cd22cc66aa614038857cef51b3f8e28 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 28 Mar 2025 22:43:31 +0100 Subject: [PATCH 064/258] Bump music assistant client to 1.2.0 (#141668) * Bump music assistant client to 1.2.0 * Update test fixtures --- .../components/music_assistant/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../music_assistant/fixtures/players.json | 42 +++++++++++++------ 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/music_assistant/manifest.json b/homeassistant/components/music_assistant/manifest.json index fb8bb9c3ac2..28e8587e90c 100644 --- a/homeassistant/components/music_assistant/manifest.json +++ b/homeassistant/components/music_assistant/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/music_assistant", "iot_class": "local_push", "loggers": ["music_assistant"], - "requirements": ["music-assistant-client==1.1.1"], + "requirements": ["music-assistant-client==1.2.0"], "zeroconf": ["_mass._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index a439774dd30..46cfb651f2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1453,7 +1453,7 @@ mozart-api==4.1.1.116.4 mullvad-api==1.0.0 # homeassistant.components.music_assistant -music-assistant-client==1.1.1 +music-assistant-client==1.2.0 # homeassistant.components.tts mutagen==1.47.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69d32bf1f33..2060886e063 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1223,7 +1223,7 @@ mozart-api==4.1.1.116.4 mullvad-api==1.0.0 # homeassistant.components.music_assistant -music-assistant-client==1.1.1 +music-assistant-client==1.2.0 # homeassistant.components.tts mutagen==1.47.0 diff --git a/tests/components/music_assistant/fixtures/players.json b/tests/components/music_assistant/fixtures/players.json index 8a08a55dc45..e8978f17f86 100644 --- a/tests/components/music_assistant/fixtures/players.json +++ b/tests/components/music_assistant/fixtures/players.json @@ -34,12 +34,16 @@ "needs_poll": false, "poll_interval": 30, "enabled": true, - "hidden": false, "icon": "mdi-speaker", "group_volume": 20, "display_name": "Test Player 1", - "extra_data": {}, - "announcement_in_progress": false + "power_control": "native", + "volume_control": "native", + "mute_control": "native", + "hide_player_in_ui": ["when_unavailable"], + "expose_to_ha": true, + "can_group_with": ["00:00:00:00:00:02"], + "source_list": [] }, { "player_id": "00:00:00:00:00:02", @@ -83,15 +87,27 @@ }, "synced_to": null, "enabled_by_default": true, - "needs_poll": false, - "poll_interval": 30, "enabled": true, "hidden": false, "icon": "mdi-speaker", "group_volume": 20, "display_name": "My Super Test Player 2", - "extra_data": {}, - "announcement_in_progress": false + "power_control": "native", + "volume_control": "native", + "mute_control": "native", + "hide_player_in_ui": ["when_unavailable"], + "expose_to_ha": true, + "can_group_with": ["00:00:00:00:00:01"], + "source_list": [ + { + "id": "spotify", + "name": "Spotify Connect", + "passive": true, + "can_play_pause": false, + "can_seek": false, + "can_next_previous": false + } + ] }, { "player_id": "test_group_player_1", @@ -135,15 +151,17 @@ }, "synced_to": null, "enabled_by_default": true, - "needs_poll": true, - "poll_interval": 30, "enabled": true, - "hidden": false, "icon": "mdi-speaker-multiple", "group_volume": 6, "display_name": "Test Group Player 1", - "extra_data": {}, - "announcement_in_progress": false + "power_control": "native", + "volume_control": "native", + "mute_control": "native", + "hide_player_in_ui": ["when_unavailable"], + "expose_to_ha": true, + "can_group_with": [], + "source_list": [] } ] } From 405cbd6a00d0f77916ed1e580ba19708223f77d1 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 29 Mar 2025 18:39:59 +0100 Subject: [PATCH 065/258] Always set pause feature on Music Assistant mediaplayers (#141686) --- .../components/music_assistant/media_player.py | 8 ++++++-- .../components/music_assistant/test_media_player.py | 13 ------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index 56bde7bbae7..7d26f5b3a0c 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -94,6 +94,12 @@ SUPPORTED_FEATURES_BASE = ( | MediaPlayerEntityFeature.MEDIA_ENQUEUE | MediaPlayerEntityFeature.MEDIA_ANNOUNCE | MediaPlayerEntityFeature.SEEK + # we always add pause support, + # regardless if the underlying player actually natively supports pause + # because the MA behavior is to internally handle pause with stop + # (and a resume position) and we'd like to keep the UX consistent + # background info: https://github.com/home-assistant/core/issues/140118 + | MediaPlayerEntityFeature.PAUSE ) QUEUE_OPTION_MAP = { @@ -697,8 +703,6 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): supported_features = SUPPORTED_FEATURES_BASE if PlayerFeature.SET_MEMBERS in self.player.supported_features: supported_features |= MediaPlayerEntityFeature.GROUPING - if PlayerFeature.PAUSE in self.player.supported_features: - supported_features |= MediaPlayerEntityFeature.PAUSE if self.player.mute_control != PLAYER_CONTROL_NONE: supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE if self.player.volume_control != PLAYER_CONTROL_NONE: diff --git a/tests/components/music_assistant/test_media_player.py b/tests/components/music_assistant/test_media_player.py index 44317d4977a..ad321a1cc29 100644 --- a/tests/components/music_assistant/test_media_player.py +++ b/tests/components/music_assistant/test_media_player.py @@ -694,19 +694,6 @@ async def test_media_player_supported_features( assert state assert state.attributes["supported_features"] == expected_features - # remove pause capability from player, trigger subscription callback - # and check if the supported features got updated - music_assistant_client.players._players[mass_player_id].supported_features.remove( - PlayerFeature.PAUSE - ) - await trigger_subscription_callback( - hass, music_assistant_client, EventType.PLAYER_CONFIG_UPDATED, mass_player_id - ) - expected_features &= ~MediaPlayerEntityFeature.PAUSE - state = hass.states.get(entity_id) - assert state - assert state.attributes["supported_features"] == expected_features - # remove grouping capability from player, trigger subscription callback # and check if the supported features got updated music_assistant_client.players._players[mass_player_id].supported_features.remove( From a0bd8deee93294d48659a933df7a130a3feb24d1 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 28 Mar 2025 22:43:00 +0100 Subject: [PATCH 066/258] Replace "country" with common string in `holiday` (#141687) --- homeassistant/components/holiday/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/holiday/strings.json b/homeassistant/components/holiday/strings.json index d464f9e8bfd..6e317b8fa7b 100644 --- a/homeassistant/components/holiday/strings.json +++ b/homeassistant/components/holiday/strings.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "country": "Country" + "country": "[%key:common::config_flow::data::country%]" } }, "options": { From 6ad5e9e89c9de664d00903261b46a56ece700b70 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 29 Mar 2025 17:29:04 +0100 Subject: [PATCH 067/258] Improve MQTT translation strings (#141691) * Improve MQTT options translation string * more improvements --- 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 95cef3119b4..2bc8ff3b71f 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -219,10 +219,10 @@ "options": "Add option" }, "data_description": { - "device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)", - "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)", + "device_class": "The Device class of the {platform} entity. [Learn more.]({url}#device_class)", + "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)", "unit_of_measurement": "Defines the unit of measurement of the sensor, if any.", - "options": "Options for allowed sensor state values. The sensor’s device_class must be set to Enumeration. The options option cannot be used together with State Class or Unit of measurement." + "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." }, "sections": { "advanced_settings": { From df777318d13d971eedaf1e37ff4e6d6a3716245a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 Mar 2025 00:58:41 +0100 Subject: [PATCH 068/258] Handle invalid JSON errors in AirNow (#141695) --- homeassistant/components/airnow/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airnow/coordinator.py b/homeassistant/components/airnow/coordinator.py index ee5bf4a1dd7..1e73bc7551e 100644 --- a/homeassistant/components/airnow/coordinator.py +++ b/homeassistant/components/airnow/coordinator.py @@ -8,7 +8,7 @@ from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectorError from pyairnow import WebServiceAPI from pyairnow.conv import aqi_to_concentration -from pyairnow.errors import AirNowError +from pyairnow.errors import AirNowError, InvalidJsonError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): distance=self.distance, ) - except (AirNowError, ClientConnectorError) as error: + except (AirNowError, ClientConnectorError, InvalidJsonError) as error: raise UpdateFailed(error) from error if not obs: From 28e1843ff9e1ddc7395075b3e6cf8f47da47f6e6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 Mar 2025 00:57:56 +0100 Subject: [PATCH 069/258] Fix Tuya tdq category to pick up temp & humid (#141698) --- homeassistant/components/tuya/sensor.py | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index b1150be306a..29bdffe1c28 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -454,6 +454,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + TuyaSensorEntityDescription( + key=DPCode.VA_TEMPERATURE, + translation_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + translation_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.VA_HUMIDITY, + translation_key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + translation_key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.BRIGHT_VALUE, + translation_key="illuminance", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + *BATTERY_SENSORS, ), # Luminance Sensor # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8 From 400131df7818da07622e1dcf13ea62a0810b258f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Mar 2025 06:53:01 -1000 Subject: [PATCH 070/258] Fix ESPHome update entities being loaded before device_info is available (#141704) * Fix ESPHome update entities being loaded before device_info is available Since we load platforms when restoring config, the update platform could be loaded before the connection to the device was finished which meant device_info could still be empty. Wait until device_info is available to load the update platform. fixes #135906 * Apply suggestions from code review * move comment * Update entry_data.py Co-authored-by: TheJulianJES --------- Co-authored-by: TheJulianJES --- .../components/esphome/entry_data.py | 19 +++--- tests/components/esphome/test_update.py | 58 ++++++++++--------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index fc41ee99a00..0d8a90a94d4 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -282,15 +282,18 @@ class RuntimeEntryData: ) -> None: """Distribute an update of static infos to all platforms.""" # First, load all platforms - needed_platforms = set() - if async_get_dashboard(hass): - needed_platforms.add(Platform.UPDATE) + needed_platforms: set[Platform] = set() - if self.device_info and self.device_info.voice_assistant_feature_flags_compat( - self.api_version - ): - needed_platforms.add(Platform.BINARY_SENSOR) - needed_platforms.add(Platform.SELECT) + if self.device_info: + if async_get_dashboard(hass): + # Only load the update platform if the device_info is set + # When we restore the entry, the device_info may not be set yet + # and we don't want to load the update platform since it needs + # a complete device_info. + needed_platforms.add(Platform.UPDATE) + if self.device_info.voice_assistant_feature_flags_compat(self.api_version): + needed_platforms.add(Platform.BINARY_SENSOR) + needed_platforms.add(Platform.SELECT) ent_reg = er.async_get(hass) registry_get_entity = ent_reg.async_get_entity_id diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 5060471f5d2..76c0a9b1a70 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -86,26 +86,28 @@ def stub_reconnect(): ) async def test_update_entity( hass: HomeAssistant, - stub_reconnect, - mock_config_entry, - mock_device_info, mock_dashboard: dict[str, Any], - devices_payload, - expected_state, - expected_attributes, + devices_payload: list[dict[str, Any]], + expected_state: str, + expected_attributes: dict[str, Any], + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], ) -> None: """Test ESPHome update entity.""" mock_dashboard["configured"] = devices_payload await async_get_dashboard(hass).async_refresh() - with patch( - "homeassistant.components.esphome.update.DomainData.get_entry_data", - return_value=Mock(available=True, device_info=mock_device_info, info={}), - ): - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + ) - state = hass.states.get("update.none_firmware") + state = hass.states.get("update.test_firmware") assert state is not None assert state.state == expected_state for key, expected_value in expected_attributes.items(): @@ -130,7 +132,7 @@ async def test_update_entity( await hass.services.async_call( "update", "install", - {"entity_id": "update.none_firmware"}, + {"entity_id": "update.test_firmware"}, blocking=True, ) @@ -155,7 +157,7 @@ async def test_update_entity( await hass.services.async_call( "update", "install", - {"entity_id": "update.none_firmware"}, + {"entity_id": "update.test_firmware"}, blocking=True, ) @@ -177,7 +179,7 @@ async def test_update_entity( await hass.services.async_call( "update", "install", - {"entity_id": "update.none_firmware"}, + {"entity_id": "update.test_firmware"}, blocking=True, ) @@ -274,28 +276,30 @@ async def test_update_device_state_for_availability( async def test_update_entity_dashboard_not_available_startup( hass: HomeAssistant, - stub_reconnect, - mock_config_entry, - mock_device_info, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], mock_dashboard: dict[str, Any], ) -> None: """Test ESPHome update entity when dashboard is not available at startup.""" with ( - patch( - "homeassistant.components.esphome.update.DomainData.get_entry_data", - return_value=Mock(available=True, device_info=mock_device_info, info={}), - ), patch( "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", side_effect=TimeoutError, ), ): await async_get_dashboard(hass).async_refresh() - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + ) # We have a dashboard but it is not available - state = hass.states.get("update.none_firmware") + state = hass.states.get("update.test_firmware") assert state is None mock_dashboard["configured"] = [ @@ -308,7 +312,7 @@ async def test_update_entity_dashboard_not_available_startup( await async_get_dashboard(hass).async_refresh() await hass.async_block_till_done() - state = hass.states.get("update.none_firmware") + state = hass.states.get("update.test_firmware") assert state.state == STATE_ON expected_attributes = { "latest_version": "2023.2.0-dev", From 5459daaa101ecc5d9506fd36fca3e7a7fd45548e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Mar 2025 06:48:51 -1000 Subject: [PATCH 071/258] Fix ESPHome entities not being removed when the ESPHome config removes an entire platform (#141708) * Fix old ESPHome entities not being removed when configuration changes fixes #140756 * make sure all callbacks fire * make sure all callbacks fire * make sure all callbacks fire * make sure all callbacks fire * revert * cover --- .../components/esphome/entry_data.py | 21 +++--- tests/components/esphome/test_entity.py | 70 +++++++++++++++++++ 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 0d8a90a94d4..023c6f70da4 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -315,18 +315,19 @@ class RuntimeEntryData: # Make a dict of the EntityInfo by type and send # them to the listeners for each specific EntityInfo type - infos_by_type: dict[type[EntityInfo], list[EntityInfo]] = {} + infos_by_type: defaultdict[type[EntityInfo], list[EntityInfo]] = defaultdict( + list + ) for info in infos: - info_type = type(info) - if info_type not in infos_by_type: - infos_by_type[info_type] = [] - infos_by_type[info_type].append(info) + infos_by_type[type(info)].append(info) - callbacks_by_type = self.entity_info_callbacks - for type_, entity_infos in infos_by_type.items(): - if callbacks_ := callbacks_by_type.get(type_): - for callback_ in callbacks_: - callback_(entity_infos) + for type_, callbacks in self.entity_info_callbacks.items(): + # If all entities for a type are removed, we + # still need to call the callbacks with an empty list + # to make sure the entities are removed. + entity_infos = infos_by_type.get(type_, []) + for callback_ in callbacks: + callback_(entity_infos) # Finally update static info subscriptions for callback_ in self.static_info_update_subscriptions: diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 296d61b664d..977ec50ab30 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -260,6 +260,76 @@ async def test_entities_removed_after_reload( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1 +async def test_entities_for_entire_platform_removed( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_client: APIClient, + hass_storage: dict[str, Any], + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test removing all entities for a specific platform when static info changes.""" + entity_info = [ + BinarySensorInfo( + object_id="mybinary_sensor_to_be_removed", + key=1, + name="my binary_sensor to be removed", + unique_id="mybinary_sensor_to_be_removed", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + ] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + entry = mock_device.entry + entry_id = entry.entry_id + storage_key = f"esphome.{entry_id}" + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + assert state is not None + assert state.state == STATE_ON + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1 + + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + assert state is not None + reg_entry = entity_registry.async_get( + "binary_sensor.test_mybinary_sensor_to_be_removed" + ) + assert reg_entry is not None + assert state.attributes[ATTR_RESTORED] is True + + entity_info = [] + states = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + entry=entry, + ) + assert mock_device.entry.entry_id == entry_id + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + assert state is None + reg_entry = entity_registry.async_get( + "binary_sensor.test_mybinary_sensor_to_be_removed" + ) + assert reg_entry is None + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 0 + + async def test_entity_info_object_ids( hass: HomeAssistant, mock_client: APIClient, From e77a1b12f73ec88d04d003340177967fd397ea73 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sat, 29 Mar 2025 10:19:25 +0100 Subject: [PATCH 072/258] Sentence-case "Medium type" in `mopeka` (#141718) --- homeassistant/components/mopeka/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mopeka/strings.json b/homeassistant/components/mopeka/strings.json index 2455eea2f76..23feb554772 100644 --- a/homeassistant/components/mopeka/strings.json +++ b/homeassistant/components/mopeka/strings.json @@ -6,7 +6,7 @@ "description": "[%key:component::bluetooth::config::step::user::description%]", "data": { "address": "[%key:common::config_flow::data::device%]", - "medium_type": "Medium Type" + "medium_type": "Medium type" } }, "bluetooth_confirm": { From 1244fc468281b26779ba39e8bd0fa26ca710b072 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Mar 2025 10:18:27 +0100 Subject: [PATCH 073/258] Only link the parent device if known in SmartThings (#141719) Only link the parent device if we know the parent device --- homeassistant/components/smartthings/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 4f7b8c2ddb9..346d5e66b42 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -426,7 +426,7 @@ def create_devices( kwargs[ATTR_CONNECTIONS] = { (dr.CONNECTION_NETWORK_MAC, device.device.hub.mac_address) } - if device.device.parent_device_id: + if device.device.parent_device_id and device.device.parent_device_id in devices: kwargs[ATTR_VIA_DEVICE] = (DOMAIN, device.device.parent_device_id) if (ocf := device.device.ocf) is not None: kwargs.update( From 9927de4801ec14426dedcd0d853f4e0884ef510d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Mar 2025 10:17:38 +0100 Subject: [PATCH 074/258] Only trigger events on button updates in SmartThings (#141720) Only trigger events on button updates --- homeassistant/components/smartthings/event.py | 5 ++- tests/components/smartthings/test_event.py | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/event.py b/homeassistant/components/smartthings/event.py index 8b413f04713..0439e6391f4 100644 --- a/homeassistant/components/smartthings/event.py +++ b/homeassistant/components/smartthings/event.py @@ -58,5 +58,6 @@ class SmartThingsButtonEvent(SmartThingsEntity, EventEntity): ) def _update_handler(self, event: DeviceEvent) -> None: - self._trigger_event(cast(str, event.value)) - self.async_write_ha_state() + if event.attribute is Attribute.BUTTON: + self._trigger_event(cast(str, event.value)) + super()._update_handler(event) diff --git a/tests/components/smartthings/test_event.py b/tests/components/smartthings/test_event.py index bdca7674981..34a96e9c6b4 100644 --- a/tests/components/smartthings/test_event.py +++ b/tests/components/smartthings/test_event.py @@ -7,6 +7,7 @@ from pysmartthings import Attribute, Capability import pytest from syrupy import SnapshotAssertion +from homeassistant.components.event import ATTR_EVENT_TYPES from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -59,3 +60,40 @@ async def test_state_update( hass.states.get("event.livingroom_smart_switch_button1").state == "2023-10-21T00:00:00.000+00:00" ) + + +@pytest.mark.parametrize("device_fixture", ["heatit_zpushwall"]) +async def test_supported_button_values_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test supported button values update.""" + await setup_integration(hass, mock_config_entry) + + freezer.move_to("2023-10-21") + + assert ( + hass.states.get("event.livingroom_smart_switch_button1").state == STATE_UNKNOWN + ) + assert hass.states.get("event.livingroom_smart_switch_button1").attributes[ + ATTR_EVENT_TYPES + ] == ["pushed", "held", "down_hold"] + + await trigger_update( + hass, + devices, + "5e5b97f3-3094-44e6-abc0-f61283412d6a", + Capability.BUTTON, + Attribute.SUPPORTED_BUTTON_VALUES, + ["pushed", "held", "down_hold", "pushed_2x"], + component="button1", + ) + + assert ( + hass.states.get("event.livingroom_smart_switch_button1").state == STATE_UNKNOWN + ) + assert hass.states.get("event.livingroom_smart_switch_button1").attributes[ + ATTR_EVENT_TYPES + ] == ["pushed", "held", "down_hold", "pushed_2x"] From f504a759e0985d082f201c64f84e45fdaabc5459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Sat, 29 Mar 2025 15:02:54 +0100 Subject: [PATCH 075/258] Set Home Connect program action field as not required (#141729) * Set Home Connect program action field as not required * Remove required field Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/home_connect/services.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/home_connect/services.yaml b/homeassistant/components/home_connect/services.yaml index 2b53090fd34..e07e8e91457 100644 --- a/homeassistant/components/home_connect/services.yaml +++ b/homeassistant/components/home_connect/services.yaml @@ -64,7 +64,6 @@ set_program_and_options: - selected_program program: example: dishcare_dishwasher_program_auto2 - required: true selector: select: mode: dropdown From 07e9020dfa131a3503490f6e2ce114cadddecac1 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 29 Mar 2025 18:22:03 +0100 Subject: [PATCH 076/258] Fix immediate state update for Comelit (#141735) --- homeassistant/components/comelit/cover.py | 21 +++++++++++---------- homeassistant/components/comelit/light.py | 3 ++- homeassistant/components/comelit/switch.py | 3 ++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/comelit/cover.py b/homeassistant/components/comelit/cover.py index 9bcf52ac111..befcb0c35d4 100644 --- a/homeassistant/components/comelit/cover.py +++ b/homeassistant/components/comelit/cover.py @@ -8,7 +8,7 @@ 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.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -98,13 +98,20 @@ class ComelitCoverEntity( """Return if the cover is opening.""" return self._current_action("opening") + async def _cover_set_state(self, action: int, state: int) -> None: + """Set desired cover state.""" + self._last_state = self.state + await self._api.set_device_status(COVER, self._device.index, action) + self.coordinator.data[COVER][self._device.index].status = state + self.async_write_ha_state() + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - await self._api.set_device_status(COVER, self._device.index, STATE_OFF) + await self._cover_set_state(STATE_OFF, 2) async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" - await self._api.set_device_status(COVER, self._device.index, STATE_ON) + await self._cover_set_state(STATE_ON, 1) async def async_stop_cover(self, **_kwargs: Any) -> None: """Stop the cover.""" @@ -112,13 +119,7 @@ class ComelitCoverEntity( return action = STATE_ON if self.is_closing else STATE_OFF - await self._api.set_device_status(COVER, self._device.index, action) - - @callback - def _handle_coordinator_update(self) -> None: - """Handle device update.""" - self._last_state = self.state - self.async_write_ha_state() + await self._cover_set_state(action, 0) async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/comelit/light.py b/homeassistant/components/comelit/light.py index 09180d628a6..53cf6bdcb46 100644 --- a/homeassistant/components/comelit/light.py +++ b/homeassistant/components/comelit/light.py @@ -59,7 +59,8 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity): 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) - await self.coordinator.async_request_refresh() + self.coordinator.data[LIGHT][self._device.index].status = state + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" diff --git a/homeassistant/components/comelit/switch.py b/homeassistant/components/comelit/switch.py index db89bd082f6..2c751cbe2cb 100644 --- a/homeassistant/components/comelit/switch.py +++ b/homeassistant/components/comelit/switch.py @@ -67,7 +67,8 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity): await self.coordinator.api.set_device_status( self._device.type, self._device.index, state ) - await self.coordinator.async_request_refresh() + self.coordinator.data[self._device.type][self._device.index].status = state + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" From 776664930476a9bac9158d85a71f00fab8553067 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 Mar 2025 17:50:46 +0000 Subject: [PATCH 077/258] Bump version to 2025.4.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 4720dbea545..7d3282d068e 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 = 4 -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, 0) diff --git a/pyproject.toml b/pyproject.toml index 79d203b61ed..c023fe7fdaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b8" +version = "2025.4.0b9" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 5d45b84cd21cf4d4e4525a66a7c29446fdfca9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Mind=C3=AAllo=20de=20Andrade?= Date: Sun, 30 Mar 2025 14:11:09 -0300 Subject: [PATCH 078/258] Remove sunweg integration (#124230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(sunweg): remove sunweg integration * Update homeassistant/components/sunweg/strings.json Co-authored-by: Abílio Costa * Update homeassistant/components/sunweg/manifest.json Co-authored-by: Abílio Costa * feat: added async remove entry * Clean setup_entry; add tests --------- Co-authored-by: Abílio Costa Co-authored-by: abmantis --- CODEOWNERS | 2 - homeassistant/components/sunweg/__init__.py | 204 ++------------ .../components/sunweg/config_flow.py | 122 +------- homeassistant/components/sunweg/const.py | 25 -- homeassistant/components/sunweg/manifest.json | 6 +- .../components/sunweg/sensor/__init__.py | 178 ------------ .../components/sunweg/sensor/inverter.py | 70 ----- .../components/sunweg/sensor/phase.py | 27 -- .../sensor/sensor_entity_description.py | 24 -- .../components/sunweg/sensor/string.py | 27 -- .../components/sunweg/sensor/total.py | 50 ---- homeassistant/components/sunweg/strings.json | 35 +-- pyproject.toml | 2 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - .../fixtures/current_data.json | 1 - tests/components/sunweg/__init__.py | 2 +- tests/components/sunweg/common.py | 22 -- tests/components/sunweg/conftest.py | 90 ------ tests/components/sunweg/test_config_flow.py | 223 --------------- tests/components/sunweg/test_init.py | 266 +++++------------- 21 files changed, 101 insertions(+), 1281 deletions(-) delete mode 100644 homeassistant/components/sunweg/const.py delete mode 100644 homeassistant/components/sunweg/sensor/__init__.py delete mode 100644 homeassistant/components/sunweg/sensor/inverter.py delete mode 100644 homeassistant/components/sunweg/sensor/phase.py delete mode 100644 homeassistant/components/sunweg/sensor/sensor_entity_description.py delete mode 100644 homeassistant/components/sunweg/sensor/string.py delete mode 100644 homeassistant/components/sunweg/sensor/total.py delete mode 100644 tests/components/sunweg/common.py delete mode 100644 tests/components/sunweg/conftest.py delete mode 100644 tests/components/sunweg/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 9a8d8b2fc64..8afd3bab028 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1480,8 +1480,6 @@ build.json @home-assistant/supervisor /tests/components/suez_water/ @ooii @jb101010-2 /homeassistant/components/sun/ @Swamp-Ig /tests/components/sun/ @Swamp-Ig -/homeassistant/components/sunweg/ @rokam -/tests/components/sunweg/ @rokam /homeassistant/components/supla/ @mwegrzynek /homeassistant/components/surepetcare/ @benleb @danielhiversen /tests/components/surepetcare/ @benleb @danielhiversen diff --git a/homeassistant/components/sunweg/__init__.py b/homeassistant/components/sunweg/__init__.py index 86da0a247b1..0dfed0e6bb3 100644 --- a/homeassistant/components/sunweg/__init__.py +++ b/homeassistant/components/sunweg/__init__.py @@ -1,197 +1,39 @@ """The Sun WEG inverter sensor integration.""" -import datetime -import json -import logging - -from sunweg.api import APIHelper -from sunweg.plant import Plant - -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.typing import StateType, UndefinedType -from homeassistant.util import Throttle +from homeassistant.helpers import issue_registry as ir -from .const import CONF_PLANT_ID, DOMAIN, PLATFORMS, DeviceType - -SCAN_INTERVAL = datetime.timedelta(minutes=5) - -_LOGGER = logging.getLogger(__name__) +DOMAIN = "sunweg" -async def async_setup_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: """Load the saved entities.""" - api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) - if not await hass.async_add_executor_job(api.authenticate): - raise ConfigEntryAuthFailed("Username or Password may be incorrect!") - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData( - api, entry.data[CONF_PLANT_ID] + ir.async_create_issue( + hass, + DOMAIN, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "issue": "https://github.com/rokam/sunweg/issues/13", + "entries": "/config/integrations/integration/sunweg", + }, ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hass.data[DOMAIN].pop(entry.entry_id) - if len(hass.data[DOMAIN]) == 0: - hass.data.pop(DOMAIN) - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return True -class SunWEGData: - """The class for handling data retrieval.""" - - def __init__( - self, - api: APIHelper, - plant_id: int, - ) -> None: - """Initialize the probe.""" - - self.api = api - self.plant_id = plant_id - self.data: Plant = None - self.previous_values: dict = {} - - @Throttle(SCAN_INTERVAL) - def update(self) -> None: - """Update probe data.""" - _LOGGER.debug("Updating data for plant %s", self.plant_id) - try: - self.data = self.api.plant(self.plant_id) - for inverter in self.data.inverters: - self.api.complete_inverter(inverter) - except json.decoder.JSONDecodeError: - _LOGGER.error("Unable to fetch data from SunWEG server") - _LOGGER.debug("Finished updating data for plant %s", self.plant_id) - - def get_api_value( - self, - variable: str, - device_type: DeviceType, - inverter_id: int = 0, - deep_name: str | None = None, - ): - """Retrieve from a Plant the desired variable value.""" - if device_type == DeviceType.TOTAL: - return self.data.__dict__.get(variable) - - inverter_list = [i for i in self.data.inverters if i.id == inverter_id] - if len(inverter_list) == 0: - return None - inverter = inverter_list[0] - - if device_type == DeviceType.INVERTER: - return inverter.__dict__.get(variable) - if device_type == DeviceType.PHASE: - for phase in inverter.phases: - if phase.name == deep_name: - return phase.__dict__.get(variable) - elif device_type == DeviceType.STRING: - for mppt in inverter.mppts: - for string in mppt.strings: - if string.name == deep_name: - return string.__dict__.get(variable) - return None - - def get_data( - self, - *, - api_variable_key: str, - api_variable_unit: str | None, - deep_name: str | None, - device_type: DeviceType, - inverter_id: int, - name: str | UndefinedType | None, - native_unit_of_measurement: str | None, - never_resets: bool, - previous_value_drop_threshold: float | None, - ) -> tuple[StateType | datetime.datetime, str | None]: - """Get the data.""" - _LOGGER.debug( - "Data request for: %s", - name, - ) - variable = api_variable_key - previous_unit = native_unit_of_measurement - api_value = self.get_api_value(variable, device_type, inverter_id, deep_name) - previous_value = self.previous_values.get(variable) - return_value = api_value - if api_variable_unit is not None: - native_unit_of_measurement = self.get_api_value( - api_variable_unit, - device_type, - inverter_id, - deep_name, - ) - - # If we have a 'drop threshold' specified, then check it and correct if needed - if ( - previous_value_drop_threshold is not None - and previous_value is not None - and api_value is not None - and previous_unit == native_unit_of_measurement - ): - _LOGGER.debug( - ( - "%s - Drop threshold specified (%s), checking for drop... API" - " Value: %s, Previous Value: %s" - ), - name, - previous_value_drop_threshold, - api_value, - previous_value, - ) - diff = float(api_value) - float(previous_value) - - # Check if the value has dropped (negative value i.e. < 0) and it has only - # dropped by a small amount, if so, use the previous value. - # Note - The energy dashboard takes care of drops within 10% - # of the current value, however if the value is low e.g. 0.2 - # and drops by 0.1 it classes as a reset. - if -(previous_value_drop_threshold) <= diff < 0: - _LOGGER.debug( - ( - "Diff is negative, but only by a small amount therefore not a" - " nightly reset, using previous value (%s) instead of api value" - " (%s)" - ), - previous_value, - api_value, - ) - return_value = previous_value - else: - _LOGGER.debug("%s - No drop detected, using API value", name) - - # Lifetime total values should always be increasing, they will never reset, - # however the API sometimes returns 0 values when the clock turns to 00:00 - # local time in that scenario we should just return the previous value - # Scenarios: - # 1 - System has a genuine 0 value when it it first commissioned: - # - will return 0 until a non-zero value is registered - # 2 - System has been running fine but temporarily resets to 0 briefly - # at midnight: - # - will return the previous value - # 3 - HA is restarted during the midnight 'outage' - Not handled: - # - Previous value will not exist meaning 0 will be returned - # - This is an edge case that would be better handled by looking - # up the previous value of the entity from the recorder - if never_resets and api_value == 0 and previous_value: - _LOGGER.debug( - ( - "API value is 0, but this value should never reset, returning" - " previous value (%s) instead" - ), - previous_value, - ) - return_value = previous_value - - self.previous_values[variable] = return_value - - return (return_value, native_unit_of_measurement) +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry.""" + if not hass.config_entries.async_loaded_entries(DOMAIN): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) + # Remove any remaining disabled or ignored entries + for _entry in hass.config_entries.async_entries(DOMAIN): + hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id)) diff --git a/homeassistant/components/sunweg/config_flow.py b/homeassistant/components/sunweg/config_flow.py index 24df8c02f55..42535a9ef58 100644 --- a/homeassistant/components/sunweg/config_flow.py +++ b/homeassistant/components/sunweg/config_flow.py @@ -1,129 +1,11 @@ """Config flow for Sun WEG integration.""" -from collections.abc import Mapping -from typing import Any +from homeassistant.config_entries import ConfigFlow -from sunweg.api import APIHelper, SunWegApiError -import voluptuous as vol - -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback - -from .const import CONF_PLANT_ID, DOMAIN +from . import DOMAIN class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow class.""" VERSION = 1 - - def __init__(self) -> None: - """Initialise sun weg server flow.""" - self.api: APIHelper = None - self.data: dict[str, Any] = {} - - @callback - def _async_show_user_form(self, step_id: str, errors=None) -> ConfigFlowResult: - """Show the form to the user.""" - default_username = "" - if CONF_USERNAME in self.data: - default_username = self.data[CONF_USERNAME] - data_schema = vol.Schema( - { - vol.Required(CONF_USERNAME, default=default_username): str, - vol.Required(CONF_PASSWORD): str, - } - ) - - return self.async_show_form( - step_id=step_id, data_schema=data_schema, errors=errors - ) - - def _set_auth_data( - self, step: str, username: str, password: str - ) -> ConfigFlowResult | None: - """Set username and password.""" - if self.api: - # Set username and password - self.api.username = username - self.api.password = password - else: - # Initialise the library with the username & password - self.api = APIHelper(username, password) - - try: - if not self.api.authenticate(): - return self._async_show_user_form(step, {"base": "invalid_auth"}) - except SunWegApiError: - return self._async_show_user_form(step, {"base": "timeout_connect"}) - - return None - - async def async_step_user(self, user_input=None) -> ConfigFlowResult: - """Handle the start of the config flow.""" - if not user_input: - return self._async_show_user_form("user") - - # Store authentication info - self.data = user_input - - conf_result = await self.hass.async_add_executor_job( - self._set_auth_data, - "user", - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - ) - - return await self.async_step_plant() if conf_result is None else conf_result - - async def async_step_plant(self, user_input=None) -> ConfigFlowResult: - """Handle adding a "plant" to Home Assistant.""" - plant_list = await self.hass.async_add_executor_job(self.api.listPlants) - - if len(plant_list) == 0: - return self.async_abort(reason="no_plants") - - plants = {plant.id: plant.name for plant in plant_list} - - if user_input is None and len(plant_list) > 1: - data_schema = vol.Schema({vol.Required(CONF_PLANT_ID): vol.In(plants)}) - - return self.async_show_form(step_id="plant", data_schema=data_schema) - - if user_input is None and len(plant_list) == 1: - user_input = {CONF_PLANT_ID: plant_list[0].id} - - user_input[CONF_NAME] = plants[user_input[CONF_PLANT_ID]] - await self.async_set_unique_id(user_input[CONF_PLANT_ID]) - self._abort_if_unique_id_configured() - self.data.update(user_input) - return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) - - async def async_step_reauth( - self, entry_data: Mapping[str, Any] - ) -> ConfigFlowResult: - """Handle reauthorization request from SunWEG.""" - self.data.update(entry_data) - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle reauthorization flow.""" - if user_input is None: - return self._async_show_user_form("reauth_confirm") - - self.data.update(user_input) - conf_result = await self.hass.async_add_executor_job( - self._set_auth_data, - "reauth_confirm", - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - ) - if conf_result is not None: - return conf_result - - return self.async_update_reload_and_abort( - self._get_reauth_entry(), data=self.data - ) diff --git a/homeassistant/components/sunweg/const.py b/homeassistant/components/sunweg/const.py deleted file mode 100644 index 11d24352962..00000000000 --- a/homeassistant/components/sunweg/const.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Define constants for the Sun WEG component.""" - -from enum import Enum - -from homeassistant.const import Platform - - -class DeviceType(Enum): - """Device Type Enum.""" - - TOTAL = 1 - INVERTER = 2 - PHASE = 3 - STRING = 4 - - -CONF_PLANT_ID = "plant_id" - -DEFAULT_PLANT_ID = 0 - -DEFAULT_NAME = "Sun WEG" - -DOMAIN = "sunweg" - -PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/sunweg/manifest.json b/homeassistant/components/sunweg/manifest.json index 3ebe9ef8cb4..3e5c669f37f 100644 --- a/homeassistant/components/sunweg/manifest.json +++ b/homeassistant/components/sunweg/manifest.json @@ -1,10 +1,10 @@ { "domain": "sunweg", "name": "Sun WEG", - "codeowners": ["@rokam"], + "codeowners": [], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sunweg", "iot_class": "cloud_polling", - "loggers": ["sunweg"], - "requirements": ["sunweg==3.0.2"] + "loggers": [], + "requirements": [] } diff --git a/homeassistant/components/sunweg/sensor/__init__.py b/homeassistant/components/sunweg/sensor/__init__.py deleted file mode 100644 index f71d992bea9..00000000000 --- a/homeassistant/components/sunweg/sensor/__init__.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Read status of SunWEG inverters.""" - -from __future__ import annotations - -import logging -from types import MappingProxyType -from typing import Any - -from sunweg.api import APIHelper -from sunweg.device import Inverter -from sunweg.plant import Plant - -from homeassistant.components.sensor import SensorEntity -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 SunWEGData -from ..const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DOMAIN, DeviceType -from .inverter import INVERTER_SENSOR_TYPES -from .phase import PHASE_SENSOR_TYPES -from .sensor_entity_description import SunWEGSensorEntityDescription -from .string import STRING_SENSOR_TYPES -from .total import TOTAL_SENSOR_TYPES - -_LOGGER = logging.getLogger(__name__) - - -def get_device_list( - api: APIHelper, config: MappingProxyType[str, Any] -) -> tuple[list[Inverter], int]: - """Retrieve the device list for the selected plant.""" - plant_id = int(config[CONF_PLANT_ID]) - - if plant_id == DEFAULT_PLANT_ID: - plant_info: list[Plant] = api.listPlants() - plant_id = plant_info[0].id - - devices: list[Inverter] = [] - # Get a list of devices for specified plant to add sensors for. - for inverter in api.plant(plant_id).inverters: - api.complete_inverter(inverter) - devices.append(inverter) - return (devices, plant_id) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up the SunWEG sensor.""" - name = config_entry.data[CONF_NAME] - - probe: SunWEGData = hass.data[DOMAIN][config_entry.entry_id] - - devices, plant_id = await hass.async_add_executor_job( - get_device_list, probe.api, config_entry.data - ) - - entities = [ - SunWEGInverter( - probe, - name=f"{name} Total", - unique_id=f"{plant_id}-{description.key}", - description=description, - device_type=DeviceType.TOTAL, - ) - for description in TOTAL_SENSOR_TYPES - ] - - # Add sensors for each device in the specified plant. - entities.extend( - [ - SunWEGInverter( - probe, - name=f"{device.name}", - unique_id=f"{device.sn}-{description.key}", - description=description, - device_type=DeviceType.INVERTER, - inverter_id=device.id, - ) - for device in devices - for description in INVERTER_SENSOR_TYPES - ] - ) - - entities.extend( - [ - SunWEGInverter( - probe, - name=f"{device.name} {phase.name}", - unique_id=f"{device.sn}-{phase.name}-{description.key}", - description=description, - inverter_id=device.id, - device_type=DeviceType.PHASE, - deep_name=phase.name, - ) - for device in devices - for phase in device.phases - for description in PHASE_SENSOR_TYPES - ] - ) - - entities.extend( - [ - SunWEGInverter( - probe, - name=f"{device.name} {string.name}", - unique_id=f"{device.sn}-{string.name}-{description.key}", - description=description, - inverter_id=device.id, - device_type=DeviceType.STRING, - deep_name=string.name, - ) - for device in devices - for mppt in device.mppts - for string in mppt.strings - for description in STRING_SENSOR_TYPES - ] - ) - - async_add_entities(entities, True) - - -class SunWEGInverter(SensorEntity): - """Representation of a SunWEG Sensor.""" - - entity_description: SunWEGSensorEntityDescription - - def __init__( - self, - probe: SunWEGData, - name: str, - unique_id: str, - description: SunWEGSensorEntityDescription, - device_type: DeviceType, - inverter_id: int = 0, - deep_name: str | None = None, - ) -> None: - """Initialize a sensor.""" - self.probe = probe - self.entity_description = description - self.device_type = device_type - self.inverter_id = inverter_id - self.deep_name = deep_name - - self._attr_name = f"{name} {description.name}" - self._attr_unique_id = unique_id - self._attr_icon = ( - description.icon if description.icon is not None else "mdi:solar-power" - ) - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, str(probe.plant_id))}, - manufacturer="SunWEG", - name=name, - ) - - def update(self) -> None: - """Get the latest data from the Sun WEG API and updates the state.""" - self.probe.update() - ( - self._attr_native_value, - self._attr_native_unit_of_measurement, - ) = self.probe.get_data( - api_variable_key=self.entity_description.api_variable_key, - api_variable_unit=self.entity_description.api_variable_unit, - deep_name=self.deep_name, - device_type=self.device_type, - inverter_id=self.inverter_id, - name=self.entity_description.name, - native_unit_of_measurement=self.native_unit_of_measurement, - never_resets=self.entity_description.never_resets, - previous_value_drop_threshold=self.entity_description.previous_value_drop_threshold, - ) diff --git a/homeassistant/components/sunweg/sensor/inverter.py b/homeassistant/components/sunweg/sensor/inverter.py deleted file mode 100644 index 1010488b38a..00000000000 --- a/homeassistant/components/sunweg/sensor/inverter.py +++ /dev/null @@ -1,70 +0,0 @@ -"""SunWEG Sensor definitions for the Inverter type.""" - -from __future__ import annotations - -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.const import ( - UnitOfEnergy, - UnitOfFrequency, - UnitOfPower, - UnitOfTemperature, -) - -from .sensor_entity_description import SunWEGSensorEntityDescription - -INVERTER_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = ( - SunWEGSensorEntityDescription( - key="inverter_energy_today", - name="Energy today", - api_variable_key="_today_energy", - api_variable_unit="_today_energy_metric", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - suggested_display_precision=1, - ), - SunWEGSensorEntityDescription( - key="inverter_energy_total", - name="Lifetime energy output", - api_variable_key="_total_energy", - api_variable_unit="_total_energy_metric", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - suggested_display_precision=1, - state_class=SensorStateClass.TOTAL, - never_resets=True, - ), - SunWEGSensorEntityDescription( - key="inverter_frequency", - name="AC frequency", - api_variable_key="_frequency", - native_unit_of_measurement=UnitOfFrequency.HERTZ, - device_class=SensorDeviceClass.FREQUENCY, - suggested_display_precision=1, - ), - SunWEGSensorEntityDescription( - key="inverter_current_wattage", - name="Output power", - api_variable_key="_power", - api_variable_unit="_power_metric", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - suggested_display_precision=1, - ), - SunWEGSensorEntityDescription( - key="inverter_temperature", - name="Temperature", - api_variable_key="_temperature", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - icon="mdi:temperature-celsius", - suggested_display_precision=1, - ), - SunWEGSensorEntityDescription( - key="inverter_power_factor", - name="Power Factor", - api_variable_key="_power_factor", - suggested_display_precision=1, - ), -) diff --git a/homeassistant/components/sunweg/sensor/phase.py b/homeassistant/components/sunweg/sensor/phase.py deleted file mode 100644 index d9db6c7c714..00000000000 --- a/homeassistant/components/sunweg/sensor/phase.py +++ /dev/null @@ -1,27 +0,0 @@ -"""SunWEG Sensor definitions for the Phase type.""" - -from __future__ import annotations - -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import UnitOfElectricCurrent, UnitOfElectricPotential - -from .sensor_entity_description import SunWEGSensorEntityDescription - -PHASE_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = ( - SunWEGSensorEntityDescription( - key="voltage", - name="Voltage", - api_variable_key="_voltage", - native_unit_of_measurement=UnitOfElectricPotential.VOLT, - device_class=SensorDeviceClass.VOLTAGE, - suggested_display_precision=2, - ), - SunWEGSensorEntityDescription( - key="amperage", - name="Amperage", - api_variable_key="_amperage", - native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, - device_class=SensorDeviceClass.CURRENT, - suggested_display_precision=1, - ), -) diff --git a/homeassistant/components/sunweg/sensor/sensor_entity_description.py b/homeassistant/components/sunweg/sensor/sensor_entity_description.py deleted file mode 100644 index 8c792ab617f..00000000000 --- a/homeassistant/components/sunweg/sensor/sensor_entity_description.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Sensor Entity Description for the SunWEG integration.""" - -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass(frozen=True) -class SunWEGRequiredKeysMixin: - """Mixin for required keys.""" - - api_variable_key: str - - -@dataclass(frozen=True) -class SunWEGSensorEntityDescription(SensorEntityDescription, SunWEGRequiredKeysMixin): - """Describes SunWEG sensor entity.""" - - api_variable_unit: str | None = None - previous_value_drop_threshold: float | None = None - never_resets: bool = False - icon: str | None = None diff --git a/homeassistant/components/sunweg/sensor/string.py b/homeassistant/components/sunweg/sensor/string.py deleted file mode 100644 index ec59da5d20d..00000000000 --- a/homeassistant/components/sunweg/sensor/string.py +++ /dev/null @@ -1,27 +0,0 @@ -"""SunWEG Sensor definitions for the String type.""" - -from __future__ import annotations - -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import UnitOfElectricCurrent, UnitOfElectricPotential - -from .sensor_entity_description import SunWEGSensorEntityDescription - -STRING_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = ( - SunWEGSensorEntityDescription( - key="voltage", - name="Voltage", - api_variable_key="_voltage", - native_unit_of_measurement=UnitOfElectricPotential.VOLT, - device_class=SensorDeviceClass.VOLTAGE, - suggested_display_precision=2, - ), - SunWEGSensorEntityDescription( - key="amperage", - name="Amperage", - api_variable_key="_amperage", - native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, - device_class=SensorDeviceClass.CURRENT, - suggested_display_precision=1, - ), -) diff --git a/homeassistant/components/sunweg/sensor/total.py b/homeassistant/components/sunweg/sensor/total.py deleted file mode 100644 index 2b94446a165..00000000000 --- a/homeassistant/components/sunweg/sensor/total.py +++ /dev/null @@ -1,50 +0,0 @@ -"""SunWEG Sensor definitions for Totals.""" - -from __future__ import annotations - -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.const import UnitOfEnergy, UnitOfPower - -from .sensor_entity_description import SunWEGSensorEntityDescription - -TOTAL_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = ( - SunWEGSensorEntityDescription( - key="total_money_total", - name="Money lifetime", - api_variable_key="_saving", - icon="mdi:cash", - native_unit_of_measurement="R$", - suggested_display_precision=2, - ), - SunWEGSensorEntityDescription( - key="total_energy_today", - name="Energy Today", - api_variable_key="_today_energy", - api_variable_unit="_today_energy_metric", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SunWEGSensorEntityDescription( - key="total_output_power", - name="Output Power", - api_variable_key="_total_power", - native_unit_of_measurement=UnitOfPower.KILO_WATT, - device_class=SensorDeviceClass.POWER, - ), - SunWEGSensorEntityDescription( - key="total_energy_output", - name="Lifetime energy output", - api_variable_key="_total_energy", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - never_resets=True, - ), - SunWEGSensorEntityDescription( - key="last_update", - name="Last Update", - api_variable_key="_last_update", - device_class=SensorDeviceClass.DATE, - ), -) diff --git a/homeassistant/components/sunweg/strings.json b/homeassistant/components/sunweg/strings.json index 9ab7be053b1..75abf5d9271 100644 --- a/homeassistant/components/sunweg/strings.json +++ b/homeassistant/components/sunweg/strings.json @@ -1,35 +1,8 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_plants": "No plants have been found on this account", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" - }, - "step": { - "plant": { - "data": { - "plant_id": "Plant" - }, - "title": "Select your plant" - }, - "user": { - "data": { - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" - }, - "title": "Enter your Sun WEG information" - }, - "reauth_confirm": { - "data": { - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" - }, - "title": "[%key:common::config_flow::title::reauth%]" - } + "issues": { + "integration_removed": { + "title": "The SunWEG integration has been removed", + "description": "The SunWEG integration has been removed from Home Assistant.\n\nThe library that Home Assistant uses to connect with SunWEG services, [doesn't work as expected anymore, demanding daily token renew]({issue}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing SunWEG integration entries]({entries})." } } } diff --git a/pyproject.toml b/pyproject.toml index c023fe7fdaf..75aaf797fae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -472,8 +472,6 @@ filterwarnings = [ # Ignore custom pytest marks "ignore:Unknown pytest.mark.disable_autouse_fixture:pytest.PytestUnknownMarkWarning:tests.components.met", "ignore:Unknown pytest.mark.dataset:pytest.PytestUnknownMarkWarning:tests.components.screenlogic", - # https://github.com/rokam/sunweg/blob/3.1.0/sunweg/plant.py#L96 - v3.1.0 - 2024-10-02 - "ignore:The '(kwh_per_kwp|performance_rate)' property is deprecated and will return 0:DeprecationWarning:tests.components.sunweg.test_init", # -- design choice 3rd party # https://github.com/gwww/elkm1/blob/2.2.10/elkm1_lib/util.py#L8-L19 diff --git a/requirements_all.txt b/requirements_all.txt index 46cfb651f2f..d19c7f386bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2830,9 +2830,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.7.13 -# homeassistant.components.sunweg -sunweg==3.0.2 - # homeassistant.components.surepetcare surepy==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2060886e063..ac9a74542e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2289,9 +2289,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.7.13 -# homeassistant.components.sunweg -sunweg==3.0.2 - # homeassistant.components.surepetcare surepy==0.9.0 diff --git a/tests/components/analytics_insights/fixtures/current_data.json b/tests/components/analytics_insights/fixtures/current_data.json index c652a8c0154..ff1baca49ed 100644 --- a/tests/components/analytics_insights/fixtures/current_data.json +++ b/tests/components/analytics_insights/fixtures/current_data.json @@ -1050,7 +1050,6 @@ "melnor": 42, "plaato": 45, "freedompro": 26, - "sunweg": 3, "logi_circle": 18, "proxy": 16, "statsd": 4, diff --git a/tests/components/sunweg/__init__.py b/tests/components/sunweg/__init__.py index 1453483a3fd..d9dac10eeb6 100644 --- a/tests/components/sunweg/__init__.py +++ b/tests/components/sunweg/__init__.py @@ -1 +1 @@ -"""Tests for the sunweg component.""" +"""Tests for the Sun WEG integration.""" diff --git a/tests/components/sunweg/common.py b/tests/components/sunweg/common.py deleted file mode 100644 index 096113f6609..00000000000 --- a/tests/components/sunweg/common.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Common functions needed to setup tests for Sun WEG.""" - -from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME - -from tests.common import MockConfigEntry - -SUNWEG_USER_INPUT = { - CONF_USERNAME: "username", - CONF_PASSWORD: "password", -} - -SUNWEG_MOCK_ENTRY = MockConfigEntry( - domain=DOMAIN, - unique_id=0, - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_PLANT_ID: 0, - CONF_NAME: "Name", - }, -) diff --git a/tests/components/sunweg/conftest.py b/tests/components/sunweg/conftest.py deleted file mode 100644 index db94b9cc5c8..00000000000 --- a/tests/components/sunweg/conftest.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Conftest for SunWEG tests.""" - -from datetime import datetime - -import pytest -from sunweg.device import MPPT, Inverter, Phase, String -from sunweg.plant import Plant - - -@pytest.fixture -def string_fixture() -> String: - """Define String fixture.""" - return String("STR1", 450.3, 23.4, 0) - - -@pytest.fixture -def mppt_fixture(string_fixture) -> MPPT: - """Define MPPT fixture.""" - mppt = MPPT("mppt") - mppt.strings.append(string_fixture) - return mppt - - -@pytest.fixture -def phase_fixture() -> Phase: - """Define Phase fixture.""" - return Phase("PhaseA", 120.0, 3.2, 0, 0) - - -@pytest.fixture -def inverter_fixture(phase_fixture, mppt_fixture) -> Inverter: - """Define inverter fixture.""" - inverter = Inverter( - 21255, - "INVERSOR01", - "J63T233018RE074", - 23.2, - 0.0, - 0.0, - "MWh", - 0, - "kWh", - 0.0, - 1, - 0, - "kW", - ) - inverter.phases.append(phase_fixture) - inverter.mppts.append(mppt_fixture) - return inverter - - -@pytest.fixture -def plant_fixture(inverter_fixture) -> Plant: - """Define Plant fixture.""" - plant = Plant( - 123456, - "Plant #123", - 29.5, - 0.5, - 0, - 12.786912, - 24.0, - "kWh", - 332.2, - 0.012296, - datetime(2023, 2, 16, 14, 22, 37), - ) - plant.inverters.append(inverter_fixture) - return plant - - -@pytest.fixture -def plant_fixture_alternative(inverter_fixture) -> Plant: - """Define Plant fixture.""" - plant = Plant( - 123456, - "Plant #123", - 29.5, - 0.5, - 0, - 12.786912, - 24.0, - "kWh", - 332.2, - 0.012296, - None, - ) - plant.inverters.append(inverter_fixture) - return plant diff --git a/tests/components/sunweg/test_config_flow.py b/tests/components/sunweg/test_config_flow.py deleted file mode 100644 index 8103003d7fb..00000000000 --- a/tests/components/sunweg/test_config_flow.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Tests for the Sun WEG server config flow.""" - -from unittest.mock import patch - -from sunweg.api import APIHelper, SunWegApiError - -from homeassistant import config_entries -from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType - -from .common import SUNWEG_MOCK_ENTRY, SUNWEG_USER_INPUT - -from tests.common import MockConfigEntry - - -async def test_show_authenticate_form(hass: HomeAssistant) -> None: - """Test that the setup form is served.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - -async def test_incorrect_login(hass: HomeAssistant) -> None: - """Test that it shows the appropriate error when an incorrect username/password/server is entered.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch.object(APIHelper, "authenticate", return_value=False): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], SUNWEG_USER_INPUT - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "invalid_auth"} - - -async def test_server_unavailable(hass: HomeAssistant) -> None: - """Test when the SunWEG server don't respond.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch.object( - APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error") - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], SUNWEG_USER_INPUT - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "timeout_connect"} - - -async def test_reauth(hass: HomeAssistant, plant_fixture, inverter_fixture) -> None: - """Test reauth flow.""" - mock_entry = SUNWEG_MOCK_ENTRY - mock_entry.add_to_hass(hass) - - entries = hass.config_entries.async_entries() - assert len(entries) == 1 - assert entries[0].data[CONF_USERNAME] == SUNWEG_MOCK_ENTRY.data[CONF_USERNAME] - assert entries[0].data[CONF_PASSWORD] == SUNWEG_MOCK_ENTRY.data[CONF_PASSWORD] - - result = await mock_entry.start_reauth_flow(hass) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - - with patch.object(APIHelper, "authenticate", return_value=False): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=SUNWEG_USER_INPUT, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "invalid_auth"} - - with patch.object( - APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error") - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=SUNWEG_USER_INPUT, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "timeout_connect"} - - with ( - patch.object(APIHelper, "authenticate", return_value=True), - patch.object(APIHelper, "listPlants", return_value=[plant_fixture]), - patch.object(APIHelper, "plant", return_value=plant_fixture), - patch.object(APIHelper, "inverter", return_value=inverter_fixture), - patch.object(APIHelper, "complete_inverter"), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=SUNWEG_USER_INPUT, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - entries = hass.config_entries.async_entries() - - assert len(entries) == 1 - assert entries[0].data[CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] - assert entries[0].data[CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] - - -async def test_no_plants_on_account(hass: HomeAssistant) -> None: - """Test registering an integration with wrong auth then with no plants available.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch.object(APIHelper, "authenticate", return_value=False): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], SUNWEG_USER_INPUT - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "invalid_auth"} - - with ( - patch.object(APIHelper, "authenticate", return_value=True), - patch.object(APIHelper, "listPlants", return_value=[]), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], SUNWEG_USER_INPUT - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "no_plants" - - -async def test_multiple_plant_ids(hass: HomeAssistant, plant_fixture) -> None: - """Test registering an integration and finishing flow with an selected plant_id.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with ( - patch.object(APIHelper, "authenticate", return_value=True), - patch.object( - APIHelper, "listPlants", return_value=[plant_fixture, plant_fixture] - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], SUNWEG_USER_INPUT - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "plant" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PLANT_ID: 123456} - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"][CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] - assert result["data"][CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] - assert result["data"][CONF_PLANT_ID] == 123456 - - -async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: - """Test registering an integration and finishing flow with current plant_id.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with ( - patch.object(APIHelper, "authenticate", return_value=True), - patch.object( - APIHelper, - "listPlants", - return_value=[plant_fixture], - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], SUNWEG_USER_INPUT - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"][CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] - assert result["data"][CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] - assert result["data"][CONF_PLANT_ID] == 123456 - - -async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> None: - """Test entering an existing plant_id.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=123456) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with ( - patch.object(APIHelper, "authenticate", return_value=True), - patch.object( - APIHelper, - "listPlants", - return_value=[plant_fixture], - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], SUNWEG_USER_INPUT - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" diff --git a/tests/components/sunweg/test_init.py b/tests/components/sunweg/test_init.py index 6cbe38a128b..964b48aebcb 100644 --- a/tests/components/sunweg/test_init.py +++ b/tests/components/sunweg/test_init.py @@ -1,209 +1,79 @@ -"""Tests for the Sun WEG init.""" +"""Tests for the Sun WEG integration.""" -import json -from unittest.mock import MagicMock, patch - -from sunweg.api import APIHelper, SunWegApiError - -from homeassistant.components.sunweg import SunWEGData -from homeassistant.components.sunweg.const import DOMAIN, DeviceType -from homeassistant.components.sunweg.sensor.sensor_entity_description import ( - SunWEGSensorEntityDescription, +from homeassistant.components.sunweg import DOMAIN +from homeassistant.config_entries import ( + SOURCE_IGNORE, + ConfigEntryDisabler, + ConfigEntryState, ) -from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.helpers import issue_registry as ir -from .common import SUNWEG_MOCK_ENTRY +from tests.common import MockConfigEntry -async def test_methods(hass: HomeAssistant, plant_fixture, inverter_fixture) -> None: - """Test methods.""" - mock_entry = SUNWEG_MOCK_ENTRY - mock_entry.add_to_hass(hass) - - with ( - patch.object(APIHelper, "authenticate", return_value=True), - patch.object(APIHelper, "listPlants", return_value=[plant_fixture]), - patch.object(APIHelper, "plant", return_value=plant_fixture), - patch.object(APIHelper, "inverter", return_value=inverter_fixture), - patch.object(APIHelper, "complete_inverter"), - ): - assert await async_setup_component(hass, DOMAIN, mock_entry.data) - await hass.async_block_till_done() - assert await hass.config_entries.async_unload(mock_entry.entry_id) - - -async def test_setup_wrongpass(hass: HomeAssistant) -> None: - """Test setup with wrong pass.""" - mock_entry = SUNWEG_MOCK_ENTRY - mock_entry.add_to_hass(hass) - with patch.object(APIHelper, "authenticate", return_value=False): - assert await async_setup_component(hass, DOMAIN, mock_entry.data) - await hass.async_block_till_done() - - -async def test_setup_error_500(hass: HomeAssistant) -> None: - """Test setup with wrong pass.""" - mock_entry = SUNWEG_MOCK_ENTRY - mock_entry.add_to_hass(hass) - with patch.object( - APIHelper, "authenticate", side_effect=SunWegApiError("Error 500") - ): - assert await async_setup_component(hass, DOMAIN, mock_entry.data) - await hass.async_block_till_done() - - -async def test_sunwegdata_update_exception() -> None: - """Test SunWEGData exception on update.""" - api = MagicMock() - api.plant = MagicMock(side_effect=json.decoder.JSONDecodeError("Message", "Doc", 1)) - data = SunWEGData(api, 0) - data.update() - assert data.data is None - - -async def test_sunwegdata_update_success(plant_fixture) -> None: - """Test SunWEGData success on update.""" - api = MagicMock() - api.plant = MagicMock(return_value=plant_fixture) - api.complete_inverter = MagicMock() - data = SunWEGData(api, 0) - data.update() - assert data.data.id == plant_fixture.id - assert data.data.name == plant_fixture.name - assert data.data.kwh_per_kwp == plant_fixture.kwh_per_kwp - assert data.data.last_update == plant_fixture.last_update - assert data.data.performance_rate == plant_fixture.performance_rate - assert data.data.saving == plant_fixture.saving - assert len(data.data.inverters) == 1 - - -async def test_sunwegdata_update_success_alternative(plant_fixture_alternative) -> None: - """Test SunWEGData success on update.""" - api = MagicMock() - api.plant = MagicMock(return_value=plant_fixture_alternative) - api.complete_inverter = MagicMock() - data = SunWEGData(api, 0) - data.update() - assert data.data.id == plant_fixture_alternative.id - assert data.data.name == plant_fixture_alternative.name - assert data.data.kwh_per_kwp == plant_fixture_alternative.kwh_per_kwp - assert data.data.last_update == plant_fixture_alternative.last_update - assert data.data.performance_rate == plant_fixture_alternative.performance_rate - assert data.data.saving == plant_fixture_alternative.saving - assert len(data.data.inverters) == 1 - - -async def test_sunwegdata_get_api_value_none(plant_fixture) -> None: - """Test SunWEGData none return on get_api_value.""" - api = MagicMock() - data = SunWEGData(api, 123456) - data.data = plant_fixture - assert data.get_api_value("variable", DeviceType.INVERTER, 0, "deep_name") is None - assert data.get_api_value("variable", DeviceType.STRING, 21255, "deep_name") is None - - -async def test_sunwegdata_get_data_drop_threshold() -> None: - """Test SunWEGData get_data with drop threshold.""" - api = MagicMock() - data = SunWEGData(api, 123456) - data.get_api_value = MagicMock() - entity_description = SunWEGSensorEntityDescription( - api_variable_key="variable", key="key", previous_value_drop_threshold=0.1 +async def test_sunweg_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the Sun WEG configuration entry loading/unloading handles the repair.""" + config_entry_1 = MockConfigEntry( + title="Example 1", + domain=DOMAIN, ) - data.get_api_value.return_value = 3.0 - assert data.get_data( - api_variable_key=entity_description.api_variable_key, - api_variable_unit=entity_description.api_variable_unit, - deep_name=None, - device_type=DeviceType.TOTAL, - inverter_id=0, - name=entity_description.name, - native_unit_of_measurement=entity_description.native_unit_of_measurement, - never_resets=entity_description.never_resets, - previous_value_drop_threshold=entity_description.previous_value_drop_threshold, - ) == (3.0, None) - data.get_api_value.return_value = 2.91 - assert data.get_data( - api_variable_key=entity_description.api_variable_key, - api_variable_unit=entity_description.api_variable_unit, - deep_name=None, - device_type=DeviceType.TOTAL, - inverter_id=0, - name=entity_description.name, - native_unit_of_measurement=entity_description.native_unit_of_measurement, - never_resets=entity_description.never_resets, - previous_value_drop_threshold=entity_description.previous_value_drop_threshold, - ) == (3.0, None) - data.get_api_value.return_value = 2.8 - assert data.get_data( - api_variable_key=entity_description.api_variable_key, - api_variable_unit=entity_description.api_variable_unit, - deep_name=None, - device_type=DeviceType.TOTAL, - inverter_id=0, - name=entity_description.name, - native_unit_of_measurement=entity_description.native_unit_of_measurement, - never_resets=entity_description.never_resets, - previous_value_drop_threshold=entity_description.previous_value_drop_threshold, - ) == (2.8, None) + config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.LOADED - -async def test_sunwegdata_get_data_never_reset() -> None: - """Test SunWEGData get_data with never reset.""" - api = MagicMock() - data = SunWEGData(api, 123456) - data.get_api_value = MagicMock() - entity_description = SunWEGSensorEntityDescription( - api_variable_key="variable", key="key", never_resets=True + # Add a second one + config_entry_2 = MockConfigEntry( + title="Example 2", + domain=DOMAIN, ) - data.get_api_value.return_value = 3.0 - assert data.get_data( - api_variable_key=entity_description.api_variable_key, - api_variable_unit=entity_description.api_variable_unit, - deep_name=None, - device_type=DeviceType.TOTAL, - inverter_id=0, - name=entity_description.name, - native_unit_of_measurement=entity_description.native_unit_of_measurement, - never_resets=entity_description.never_resets, - previous_value_drop_threshold=entity_description.previous_value_drop_threshold, - ) == (3.0, None) - data.get_api_value.return_value = 0 - assert data.get_data( - api_variable_key=entity_description.api_variable_key, - api_variable_unit=entity_description.api_variable_unit, - deep_name=None, - device_type=DeviceType.TOTAL, - inverter_id=0, - name=entity_description.name, - native_unit_of_measurement=entity_description.native_unit_of_measurement, - never_resets=entity_description.never_resets, - previous_value_drop_threshold=entity_description.previous_value_drop_threshold, - ) == (3.0, None) - data.get_api_value.return_value = 2.8 - assert data.get_data( - api_variable_key=entity_description.api_variable_key, - api_variable_unit=entity_description.api_variable_unit, - deep_name=None, - device_type=DeviceType.TOTAL, - inverter_id=0, - name=entity_description.name, - native_unit_of_measurement=entity_description.native_unit_of_measurement, - never_resets=entity_description.never_resets, - previous_value_drop_threshold=entity_description.previous_value_drop_threshold, - ) == (2.8, None) + config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) -async def test_reauth_started(hass: HomeAssistant) -> None: - """Test reauth flow started.""" - mock_entry = SUNWEG_MOCK_ENTRY - mock_entry.add_to_hass(hass) - with patch.object(APIHelper, "authenticate", return_value=False): - await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - assert mock_entry.state is ConfigEntryState.SETUP_ERROR - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth_confirm" + # Add an ignored entry + config_entry_3 = MockConfigEntry( + source=SOURCE_IGNORE, + domain=DOMAIN, + ) + config_entry_3.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_3.entry_id) + await hass.async_block_till_done() + + assert config_entry_3.state is ConfigEntryState.NOT_LOADED + + # Add a disabled entry + config_entry_4 = MockConfigEntry( + disabled_by=ConfigEntryDisabler.USER, + domain=DOMAIN, + ) + config_entry_4.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_4.entry_id) + await hass.async_block_till_done() + + assert config_entry_4.state is ConfigEntryState.NOT_LOADED + + # Remove the first one + await hass.config_entries.async_remove(config_entry_1.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the second one + await hass.config_entries.async_remove(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None + + # Check the ignored and disabled entries are removed + assert not hass.config_entries.async_entries(DOMAIN) From 35fc81b038ccb9f83040f4996f5d632ab585ffb4 Mon Sep 17 00:00:00 2001 From: Michal Schwarz Date: Sun, 30 Mar 2025 16:14:56 +0200 Subject: [PATCH 079/258] Fix order of palettes, presets and playlists in WLED integration (#132207) * Fix order of palettes, presets and playlists in WLED integration * fix tests: update palette items order --------- Co-authored-by: Franck Nijhof --- homeassistant/components/wled/select.py | 21 +- .../wled/snapshots/test_select.ambr | 228 +++++++++--------- 2 files changed, 126 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index e340c323151..76837652ae5 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -79,9 +79,10 @@ class WLEDPresetSelect(WLEDEntity, SelectEntity): super().__init__(coordinator=coordinator) self._attr_unique_id = f"{coordinator.data.info.mac_address}_preset" - self._attr_options = [ - preset.name for preset in self.coordinator.data.presets.values() - ] + sorted_values = sorted( + coordinator.data.presets.values(), key=lambda preset: preset.name + ) + self._attr_options = [preset.name for preset in sorted_values] @property def available(self) -> bool: @@ -115,9 +116,10 @@ class WLEDPlaylistSelect(WLEDEntity, SelectEntity): super().__init__(coordinator=coordinator) self._attr_unique_id = f"{coordinator.data.info.mac_address}_playlist" - self._attr_options = [ - playlist.name for playlist in self.coordinator.data.playlists.values() - ] + sorted_values = sorted( + coordinator.data.playlists.values(), key=lambda playlist: playlist.name + ) + self._attr_options = [playlist.name for playlist in sorted_values] @property def available(self) -> bool: @@ -159,9 +161,10 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): self._attr_translation_placeholders = {"segment": str(segment)} self._attr_unique_id = f"{coordinator.data.info.mac_address}_palette_{segment}" - self._attr_options = [ - palette.name for palette in self.coordinator.data.palettes.values() - ] + sorted_values = sorted( + coordinator.data.palettes.values(), key=lambda palette: palette.name + ) + self._attr_options = [palette.name for palette in sorted_values] self._segment = segment @property diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index ca3b0a5dc6e..d3f8fbcc21d 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -99,77 +99,77 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'WLED RGB Light Segment 1 color palette', 'options': list([ - 'Default', - '* Random Cycle', '* Color 1', - '* Colors 1&2', '* Color Gradient', + '* Colors 1&2', '* Colors Only', - 'Party', - 'Cloud', - 'Lava', - 'Ocean', - 'Forest', - 'Rainbow', - 'Rainbow Bands', - 'Sunset', - 'Rivendell', - 'Breeze', - 'Red & Blue', - 'Yellowout', + '* Random Cycle', 'Analogous', - 'Splash', - 'Pastel', - 'Sunset 2', - 'Beach', - 'Vintage', - 'Departure', - 'Landscape', - 'Beech', - 'Sherbet', - 'Hult', - 'Hult 64', - 'Drywet', - 'Jul', - 'Grintage', - 'Rewhi', - 'Tertiary', - 'Fire', - 'Icefire', - 'Cyane', - 'Light Pink', - 'Autumn', - 'Magenta', - 'Magred', - 'Yelmag', - 'Yelblu', - 'Orange & Teal', - 'Tiamat', 'April Night', - 'Orangery', - 'C9', - 'Sakura', - 'Aurora', + 'Aqua Flash', 'Atlantica', + 'Aurora', + 'Aurora 2', + 'Autumn', + 'Beach', + 'Beech', + 'Blink Red', + 'Breeze', + 'C9', 'C9 2', 'C9 New', - 'Temperature', - 'Aurora 2', - 'Retro Clown', 'Candy', - 'Toxy Reaf', + 'Candy2', + 'Cloud', + 'Cyane', + 'Default', + 'Departure', + 'Drywet', 'Fairy Reaf', - 'Semi Blue', - 'Pink Candy', - 'Red Reaf', - 'Aqua Flash', - 'Yelblu Hot', + 'Fire', + 'Forest', + 'Grintage', + 'Hult', + 'Hult 64', + 'Icefire', + 'Jul', + 'Landscape', + 'Lava', + 'Light Pink', 'Lite Light', + 'Magenta', + 'Magred', + 'Ocean', + 'Orange & Teal', + 'Orangery', + 'Party', + 'Pastel', + 'Pink Candy', + 'Rainbow', + 'Rainbow Bands', + 'Red & Blue', 'Red Flash', - 'Blink Red', + 'Red Reaf', 'Red Shift', 'Red Tide', - 'Candy2', + 'Retro Clown', + 'Rewhi', + 'Rivendell', + 'Sakura', + 'Semi Blue', + 'Sherbet', + 'Splash', + 'Sunset', + 'Sunset 2', + 'Temperature', + 'Tertiary', + 'Tiamat', + 'Toxy Reaf', + 'Vintage', + 'Yelblu', + 'Yelblu Hot', + 'Yellowout', + 'Yelmag', ]), }), 'context': , @@ -187,77 +187,77 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ - 'Default', - '* Random Cycle', '* Color 1', - '* Colors 1&2', '* Color Gradient', + '* Colors 1&2', '* Colors Only', - 'Party', - 'Cloud', - 'Lava', - 'Ocean', - 'Forest', - 'Rainbow', - 'Rainbow Bands', - 'Sunset', - 'Rivendell', - 'Breeze', - 'Red & Blue', - 'Yellowout', + '* Random Cycle', 'Analogous', - 'Splash', - 'Pastel', - 'Sunset 2', - 'Beach', - 'Vintage', - 'Departure', - 'Landscape', - 'Beech', - 'Sherbet', - 'Hult', - 'Hult 64', - 'Drywet', - 'Jul', - 'Grintage', - 'Rewhi', - 'Tertiary', - 'Fire', - 'Icefire', - 'Cyane', - 'Light Pink', - 'Autumn', - 'Magenta', - 'Magred', - 'Yelmag', - 'Yelblu', - 'Orange & Teal', - 'Tiamat', 'April Night', - 'Orangery', - 'C9', - 'Sakura', - 'Aurora', + 'Aqua Flash', 'Atlantica', + 'Aurora', + 'Aurora 2', + 'Autumn', + 'Beach', + 'Beech', + 'Blink Red', + 'Breeze', + 'C9', 'C9 2', 'C9 New', - 'Temperature', - 'Aurora 2', - 'Retro Clown', 'Candy', - 'Toxy Reaf', + 'Candy2', + 'Cloud', + 'Cyane', + 'Default', + 'Departure', + 'Drywet', 'Fairy Reaf', - 'Semi Blue', - 'Pink Candy', - 'Red Reaf', - 'Aqua Flash', - 'Yelblu Hot', + 'Fire', + 'Forest', + 'Grintage', + 'Hult', + 'Hult 64', + 'Icefire', + 'Jul', + 'Landscape', + 'Lava', + 'Light Pink', 'Lite Light', + 'Magenta', + 'Magred', + 'Ocean', + 'Orange & Teal', + 'Orangery', + 'Party', + 'Pastel', + 'Pink Candy', + 'Rainbow', + 'Rainbow Bands', + 'Red & Blue', 'Red Flash', - 'Blink Red', + 'Red Reaf', 'Red Shift', 'Red Tide', - 'Candy2', + 'Retro Clown', + 'Rewhi', + 'Rivendell', + 'Sakura', + 'Semi Blue', + 'Sherbet', + 'Splash', + 'Sunset', + 'Sunset 2', + 'Temperature', + 'Tertiary', + 'Tiamat', + 'Toxy Reaf', + 'Vintage', + 'Yelblu', + 'Yelblu Hot', + 'Yellowout', + 'Yelmag', ]), }), 'config_entry_id': , From 85ad29e28e6ca233536adadfd68eed3b3a901375 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Mar 2025 20:31:08 +0200 Subject: [PATCH 080/258] Ensure EcoNet operation modes are unique (#141689) --- homeassistant/components/econet/water_heater.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index fb74ae8b4a5..f93ad7f8872 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -91,15 +91,15 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity): def operation_list(self) -> list[str]: """List of available operation modes.""" econet_modes = self.water_heater.modes - op_list = [] + operation_modes = set() for mode in econet_modes: if ( mode is not WaterHeaterOperationMode.UNKNOWN and mode is not WaterHeaterOperationMode.VACATION ): ha_mode = ECONET_STATE_TO_HA[mode] - op_list.append(ha_mode) - return op_list + operation_modes.add(ha_mode) + return list(operation_modes) @property def supported_features(self) -> WaterHeaterEntityFeature: From 23dbdedfb6f9de75920993d30df31fb3fd1c190f Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Sat, 29 Mar 2025 21:49:18 -0700 Subject: [PATCH 081/258] Bump iaqualink to 0.5.3 (#141709) * Update to iaqualink 0.5.3 and silence warning * Update to iaqualink 0.5.3 and silence warning * Re-add via_device line --- homeassistant/components/iaqualink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 2531632075c..7e05bd72f0b 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/iaqualink", "iot_class": "cloud_polling", "loggers": ["iaqualink"], - "requirements": ["iaqualink==0.5.0", "h2==4.1.0"], + "requirements": ["iaqualink==0.5.3", "h2==4.1.0"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index d19c7f386bb..c7ec8f1386a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1184,7 +1184,7 @@ hyperion-py==0.7.5 iammeter==0.2.1 # homeassistant.components.iaqualink -iaqualink==0.5.0 +iaqualink==0.5.3 # homeassistant.components.ibeacon ibeacon-ble==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac9a74542e7..bd6687a870a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1005,7 +1005,7 @@ huum==0.7.12 hyperion-py==0.7.5 # homeassistant.components.iaqualink -iaqualink==0.5.0 +iaqualink==0.5.3 # homeassistant.components.ibeacon ibeacon-ble==1.2.0 From eed075dbfac7b0e9f16b67339cade1681b75b3ef Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Mar 2025 21:34:49 +0200 Subject: [PATCH 082/258] Bump pySmartThings to 3.0.1 (#141722) --- 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 49de0c79ce7..2af3e5c193b 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.0.0"] + "requirements": ["pysmartthings==3.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index c7ec8f1386a..fa787fd614c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2319,7 +2319,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.0.0 +pysmartthings==3.0.1 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd6687a870a..08d43ddbcd0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1889,7 +1889,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.0.0 +pysmartthings==3.0.1 # homeassistant.components.smarty pysmarty2==0.10.2 From 8ac8401b4e71ef5a79cc24029e054d1180a6430a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:26:37 -0400 Subject: [PATCH 083/258] Add helper methods to simplify USB integration testing (#141733) * Add some helper methods to simplify USB integration testing * Re-export `usb_device_from_port` --- homeassistant/components/usb/__init__.py | 40 +-- homeassistant/components/usb/utils.py | 12 + tests/components/usb/__init__.py | 67 ++-- tests/components/usb/test_init.py | 415 +++++++++++------------ 4 files changed, 266 insertions(+), 268 deletions(-) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 994f4f71c35..90433b0f728 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -14,8 +14,6 @@ import sys from typing import Any, overload from aiousbwatcher import AIOUSBWatcher, InotifyNotAvailableError -from serial.tools.list_ports import comports -from serial.tools.list_ports_common import ListPortInfo import voluptuous as vol from homeassistant import config_entries @@ -43,7 +41,10 @@ from homeassistant.loader import USBMatcher, async_get_usb from .const import DOMAIN from .models import USBDevice -from .utils import usb_device_from_port +from .utils import ( + scan_serial_ports, + usb_device_from_port, # noqa: F401 +) _LOGGER = logging.getLogger(__name__) @@ -241,6 +242,13 @@ def _is_matching(device: USBDevice, matcher: USBMatcher | USBCallbackMatcher) -> return True +async def async_request_scan(hass: HomeAssistant) -> None: + """Request a USB scan.""" + usb_discovery: USBDiscovery = hass.data[DOMAIN] + if not usb_discovery.observer_active: + await usb_discovery.async_request_scan() + + class USBDiscovery: """Manage USB Discovery.""" @@ -417,14 +425,8 @@ class USBDiscovery: service_info, ) - async def _async_process_ports(self, ports: Sequence[ListPortInfo]) -> None: + async def _async_process_ports(self, usb_devices: Sequence[USBDevice]) -> None: """Process each discovered port.""" - _LOGGER.debug("Processing ports: %r", ports) - usb_devices = { - usb_device_from_port(port) - for port in ports - if port.vid is not None or port.pid is not None - } _LOGGER.debug("USB devices: %r", usb_devices) # CP2102N chips create *two* serial ports on macOS: `/dev/cu.usbserial-` and @@ -436,7 +438,7 @@ class USBDiscovery: if dev.device.startswith("/dev/cu.SLAB_USBtoUART") } - usb_devices = { + filtered_usb_devices = { dev for dev in usb_devices if dev.serial_number not in silabs_serials @@ -445,10 +447,12 @@ class USBDiscovery: and dev.device.startswith("/dev/cu.SLAB_USBtoUART") ) } + else: + filtered_usb_devices = set(usb_devices) - added_devices = usb_devices - self._last_processed_devices - removed_devices = self._last_processed_devices - usb_devices - self._last_processed_devices = usb_devices + added_devices = filtered_usb_devices - self._last_processed_devices + removed_devices = self._last_processed_devices - filtered_usb_devices + self._last_processed_devices = filtered_usb_devices _LOGGER.debug( "Added devices: %r, removed devices: %r", added_devices, removed_devices @@ -461,7 +465,7 @@ class USBDiscovery: except Exception: _LOGGER.exception("Error in USB port event callback") - for usb_device in usb_devices: + for usb_device in filtered_usb_devices: await self._async_process_discovered_usb_device(usb_device) @hass_callback @@ -483,7 +487,7 @@ class USBDiscovery: _LOGGER.debug("Executing comports scan") async with self._scan_lock: await self._async_process_ports( - await self.hass.async_add_executor_job(comports) + await self.hass.async_add_executor_job(scan_serial_ports) ) if self.initial_scan_done: return @@ -521,9 +525,7 @@ async def websocket_usb_scan( msg: dict[str, Any], ) -> None: """Scan for new usb devices.""" - usb_discovery: USBDiscovery = hass.data[DOMAIN] - if not usb_discovery.observer_active: - await usb_discovery.async_request_scan() + await async_request_scan(hass) connection.send_result(msg["id"]) diff --git a/homeassistant/components/usb/utils.py b/homeassistant/components/usb/utils.py index d1d6fb17f3c..1bb620ec5f7 100644 --- a/homeassistant/components/usb/utils.py +++ b/homeassistant/components/usb/utils.py @@ -2,6 +2,9 @@ from __future__ import annotations +from collections.abc import Sequence + +from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo from .models import USBDevice @@ -17,3 +20,12 @@ def usb_device_from_port(port: ListPortInfo) -> USBDevice: manufacturer=port.manufacturer, description=port.description, ) + + +def scan_serial_ports() -> Sequence[USBDevice]: + """Scan serial ports for USB devices.""" + return [ + usb_device_from_port(port) + for port in comports() + if port.vid is not None or port.pid is not None + ] diff --git a/tests/components/usb/__init__.py b/tests/components/usb/__init__.py index 96d671d0958..6db0cea1ffe 100644 --- a/tests/components/usb/__init__.py +++ b/tests/components/usb/__init__.py @@ -1,44 +1,29 @@ """Tests for the USB Discovery integration.""" -from homeassistant.components.usb.models import USBDevice +from unittest.mock import patch -conbee_device = USBDevice( - device="/dev/cu.usbmodemDE24338801", - vid="1CF1", - pid="0030", - serial_number="DE2433880", - manufacturer="dresden elektronik ingenieurtechnik GmbH", - description="ConBee II", -) -slae_sh_device = USBDevice( - device="/dev/cu.usbserial-110", - vid="10C4", - pid="EA60", - serial_number="00_12_4B_00_22_98_88_7F", - manufacturer="Silicon Labs", - description="slae.sh cc2652rb stick - slaesh's iot stuff", -) -electro_lama_device = USBDevice( - device="/dev/cu.usbserial-110", - vid="1A86", - pid="7523", - serial_number=None, - manufacturer=None, - description="USB2.0-Serial", -) -skyconnect_macos_correct = USBDevice( - device="/dev/cu.SLAB_USBtoUART", - vid="10C4", - pid="EA60", - serial_number="9ab1da1ea4b3ed11956f4eaca7669f5d", - manufacturer="Nabu Casa", - description="SkyConnect v1.0", -) -skyconnect_macos_incorrect = USBDevice( - device="/dev/cu.usbserial-2110", - vid="10C4", - pid="EA60", - serial_number="9ab1da1ea4b3ed11956f4eaca7669f5d", - manufacturer="Nabu Casa", - description="SkyConnect v1.0", -) +from aiousbwatcher import InotifyNotAvailableError +import pytest + +from homeassistant.components.usb import async_request_scan as usb_async_request_scan +from homeassistant.core import HomeAssistant + + +@pytest.fixture(name="force_usb_polling_watcher") +def force_usb_polling_watcher(): + """Patch the USB integration to not use inotify and fall back to polling.""" + with patch( + "homeassistant.components.usb.AIOUSBWatcher.async_start", + side_effect=InotifyNotAvailableError, + ): + yield + + +def patch_scanned_serial_ports(**kwargs) -> None: + """Patch the USB integration's list of scanned serial ports.""" + return patch("homeassistant.components.usb.scan_serial_ports", **kwargs) + + +async def async_request_scan(hass: HomeAssistant) -> None: + """Request a USB scan.""" + return await usb_async_request_scan(hass) diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index 9730dba53d7..3a56e929b22 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -7,31 +7,40 @@ import os from typing import Any from unittest.mock import MagicMock, Mock, call, patch, sentinel -from aiousbwatcher import InotifyNotAvailableError import pytest from homeassistant.components import usb -from homeassistant.components.usb.utils import usb_device_from_port +from homeassistant.components.usb.models import USBDevice from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.service_info.usb import UsbServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import conbee_device, slae_sh_device +from . import ( + force_usb_polling_watcher, # noqa: F401 + patch_scanned_serial_ports, +) from tests.common import async_fire_time_changed, import_and_test_deprecated_constant from tests.typing import WebSocketGenerator - -@pytest.fixture(name="aiousbwatcher_no_inotify") -def aiousbwatcher_no_inotify(): - """Patch AIOUSBWatcher to not use inotify.""" - with patch( - "homeassistant.components.usb.AIOUSBWatcher.async_start", - side_effect=InotifyNotAvailableError, - ): - yield +conbee_device = USBDevice( + device="/dev/cu.usbmodemDE24338801", + vid="1CF1", + pid="0030", + serial_number="DE2433880", + manufacturer="dresden elektronik ingenieurtechnik GmbH", + description="ConBee II", +) +slae_sh_device = USBDevice( + device="/dev/cu.usbserial-110", + vid="10C4", + pid="EA60", + serial_number="00_12_4B_00_22_98_88_7F", + manufacturer="Silicon Labs", + description="slae.sh cc2652rb stick - slaesh's iot stuff", +) async def test_aiousbwatcher_discovery( @@ -40,11 +49,11 @@ async def test_aiousbwatcher_discovery( """Test that aiousbwatcher can discover a device without raising an exception.""" new_usb = [{"domain": "test1", "vid": "3039"}, {"domain": "test2", "vid": "0FA0"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -63,7 +72,7 @@ async def test_aiousbwatcher_discovery( with ( patch("sys.platform", "linux"), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch( "homeassistant.components.usb.AIOUSBWatcher", return_value=MockAIOUSBWatcher ), @@ -81,11 +90,11 @@ async def test_aiousbwatcher_discovery( await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 - mock_comports.append( - MagicMock( + mock_ports.append( + USBDevice( device=slae_sh_device.device, - vid=4000, - pid=4000, + vid="0FA0", + pid="0FA0", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -107,7 +116,7 @@ async def test_aiousbwatcher_discovery( await hass.async_block_till_done() -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_polling_discovery( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -115,19 +124,19 @@ async def test_polling_discovery( new_usb = [{"domain": "test1", "vid": "3039"}] mock_comports_found_device = asyncio.Event() - def get_comports() -> list: - nonlocal mock_comports + def scan_serial_ports() -> list: + nonlocal mock_ports # Only "find" a device after a few invocations - if len(mock_comports.mock_calls) < 5: + if len(mock_ports.mock_calls) < 5: return [] mock_comports_found_device.set() return [ - MagicMock( + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -141,9 +150,7 @@ async def test_polling_discovery( timedelta(seconds=0.01), ), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch( - "homeassistant.components.usb.comports", side_effect=get_comports - ) as mock_comports, + patch_scanned_serial_ports(side_effect=scan_serial_ports) as mock_ports, patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -163,16 +170,16 @@ async def test_polling_discovery( await hass.async_block_till_done() -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_removal_by_aiousbwatcher_before_started(hass: HomeAssistant) -> None: """Test a device is removed by the aiousbwatcher before started.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -181,13 +188,13 @@ async def test_removal_by_aiousbwatcher_before_started(hass: HomeAssistant) -> N with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() - with patch("homeassistant.components.usb.comports", return_value=[]): + with patch_scanned_serial_ports(return_value=[]): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -197,18 +204,18 @@ async def test_removal_by_aiousbwatcher_before_started(hass: HomeAssistant) -> N await hass.async_block_till_done() -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test a device is discovered from websocket scan.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -217,7 +224,7 @@ async def test_discovered_by_websocket_scan( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -234,7 +241,7 @@ async def test_discovered_by_websocket_scan( assert mock_config_flow.mock_calls[0][1][0] == "test1" -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan_limited_by_description_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -243,11 +250,11 @@ async def test_discovered_by_websocket_scan_limited_by_description_matcher( {"domain": "test1", "vid": "3039", "pid": "3039", "description": "*2652*"} ] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -256,7 +263,7 @@ async def test_discovered_by_websocket_scan_limited_by_description_matcher( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -273,7 +280,7 @@ async def test_discovered_by_websocket_scan_limited_by_description_matcher( assert mock_config_flow.mock_calls[0][1][0] == "test1" -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_most_targeted_matcher_wins( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -283,11 +290,11 @@ async def test_most_targeted_matcher_wins( {"domain": "more", "vid": "3039", "pid": "3039", "description": "*2652*"}, ] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -296,7 +303,7 @@ async def test_most_targeted_matcher_wins( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -313,7 +320,7 @@ async def test_most_targeted_matcher_wins( assert mock_config_flow.mock_calls[0][1][0] == "more" -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan_rejected_by_description_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -322,11 +329,11 @@ async def test_discovered_by_websocket_scan_rejected_by_description_matcher( {"domain": "test1", "vid": "3039", "pid": "3039", "description": "*not_it*"} ] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -335,7 +342,7 @@ async def test_discovered_by_websocket_scan_rejected_by_description_matcher( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -351,7 +358,7 @@ async def test_discovered_by_websocket_scan_rejected_by_description_matcher( assert len(mock_config_flow.mock_calls) == 0 -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -365,11 +372,11 @@ async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( } ] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -378,7 +385,7 @@ async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -395,7 +402,7 @@ async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( assert mock_config_flow.mock_calls[0][1][0] == "test1" -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -404,11 +411,11 @@ async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( {"domain": "test1", "vid": "3039", "pid": "3039", "serial_number": "123*"} ] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -417,7 +424,7 @@ async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -433,7 +440,7 @@ async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( assert len(mock_config_flow.mock_calls) == 0 -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -447,11 +454,11 @@ async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( } ] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=conbee_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=conbee_device.serial_number, manufacturer=conbee_device.manufacturer, description=conbee_device.description, @@ -460,7 +467,7 @@ async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -477,7 +484,7 @@ async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( assert mock_config_flow.mock_calls[0][1][0] == "test1" -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -491,11 +498,11 @@ async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( } ] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=conbee_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=conbee_device.serial_number, manufacturer=conbee_device.manufacturer, description=conbee_device.description, @@ -504,7 +511,7 @@ async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -520,7 +527,7 @@ async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( assert len(mock_config_flow.mock_calls) == 0 -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -529,11 +536,11 @@ async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( {"domain": "test1", "vid": "3039", "pid": "3039", "serial_number": "123*"} ] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=conbee_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=None, manufacturer=None, description=None, @@ -542,7 +549,7 @@ async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -558,18 +565,18 @@ async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( assert len(mock_config_flow.mock_calls) == 0 -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan_match_vid_only( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test a device is discovered from websocket scan only matching vid.""" new_usb = [{"domain": "test1", "vid": "3039"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -578,7 +585,7 @@ async def test_discovered_by_websocket_scan_match_vid_only( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -595,18 +602,18 @@ async def test_discovered_by_websocket_scan_match_vid_only( assert mock_config_flow.mock_calls[0][1][0] == "test1" -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_scan_match_vid_wrong_pid( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test a device is discovered from websocket scan only matching vid but wrong pid.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "9999"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -615,7 +622,7 @@ async def test_discovered_by_websocket_scan_match_vid_wrong_pid( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -631,15 +638,15 @@ async def test_discovered_by_websocket_scan_match_vid_wrong_pid( assert len(mock_config_flow.mock_calls) == 0 -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_discovered_by_websocket_no_vid_pid( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test a device is discovered from websocket scan with no vid or pid.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "9999"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, vid=None, pid=None, @@ -651,7 +658,7 @@ async def test_discovered_by_websocket_no_vid_pid( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -667,18 +674,18 @@ async def test_discovered_by_websocket_no_vid_pid( assert len(mock_config_flow.mock_calls) == 0 -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_non_matching_discovered_by_scanner_after_started( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test a websocket scan that does not match.""" new_usb = [{"domain": "test1", "vid": "4444", "pid": "4444"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -687,7 +694,7 @@ async def test_non_matching_discovered_by_scanner_after_started( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -709,11 +716,11 @@ async def test_aiousbwatcher_on_wsl_fallback_without_throwing_exception( """Test that aiousbwatcher on WSL failure results in fallback to scanning without raising an exception.""" new_usb = [{"domain": "test1", "vid": "3039"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -722,7 +729,7 @@ async def test_aiousbwatcher_on_wsl_fallback_without_throwing_exception( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -743,17 +750,17 @@ async def test_discovered_by_aiousbwatcher_before_started(hass: HomeAssistant) - """Test a device is discovered since aiousbwatcher is now running.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, ) ] - initial_mock_comports = [] + initial_ports = [] aiousbwatcher_callback = None def async_register_callback(callback): @@ -766,9 +773,7 @@ async def test_discovered_by_aiousbwatcher_before_started(hass: HomeAssistant) - with ( patch("sys.platform", "linux"), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch( - "homeassistant.components.usb.comports", return_value=initial_mock_comports - ), + patch_scanned_serial_ports(return_value=initial_ports), patch( "homeassistant.components.usb.AIOUSBWatcher", return_value=MockAIOUSBWatcher ), @@ -782,7 +787,7 @@ async def test_discovered_by_aiousbwatcher_before_started(hass: HomeAssistant) - assert len(mock_config_flow.mock_calls) == 0 - initial_mock_comports.extend(mock_comports) + initial_ports.extend(mock_ports) aiousbwatcher_callback() await hass.async_block_till_done() @@ -874,18 +879,18 @@ def test_human_readable_device_name() -> None: assert "8A2A" in name -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_async_is_plugged_in( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test async_is_plugged_in.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -899,7 +904,7 @@ async def test_async_is_plugged_in( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=[]), + patch_scanned_serial_ports(return_value=[]), patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -909,7 +914,7 @@ async def test_async_is_plugged_in( assert not usb.async_is_plugged_in(hass, matcher) with ( - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch.object(hass.config_entries.flow, "async_init"), ): ws_client = await hass_ws_client(hass) @@ -920,7 +925,7 @@ async def test_async_is_plugged_in( assert usb.async_is_plugged_in(hass, matcher) -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") @pytest.mark.parametrize( "matcher", [ @@ -940,7 +945,7 @@ async def test_async_is_plugged_in_case_enforcement( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=[]), + patch_scanned_serial_ports(return_value=[]), patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -952,7 +957,7 @@ async def test_async_is_plugged_in_case_enforcement( usb.async_is_plugged_in(hass, matcher) -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_web_socket_triggers_discovery_request_callbacks( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -961,7 +966,7 @@ async def test_web_socket_triggers_discovery_request_callbacks( with ( patch("homeassistant.components.usb.async_get_usb", return_value=[]), - patch("homeassistant.components.usb.comports", return_value=[]), + patch_scanned_serial_ports(return_value=[]), patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -987,7 +992,7 @@ async def test_web_socket_triggers_discovery_request_callbacks( assert len(mock_callback.mock_calls) == 1 -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_initial_scan_callback( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -997,7 +1002,7 @@ async def test_initial_scan_callback( with ( patch("homeassistant.components.usb.async_get_usb", return_value=[]), - patch("homeassistant.components.usb.comports", return_value=[]), + patch_scanned_serial_ports(return_value=[]), patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -1023,7 +1028,7 @@ async def test_initial_scan_callback( cancel_2() -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_cancel_initial_scan_callback( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -1032,7 +1037,7 @@ async def test_cancel_initial_scan_callback( with ( patch("homeassistant.components.usb.async_get_usb", return_value=[]), - patch("homeassistant.components.usb.comports", return_value=[]), + patch_scanned_serial_ports(return_value=[]), patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -1049,18 +1054,18 @@ async def test_cancel_initial_scan_callback( assert len(mock_callback.mock_calls) == 0 -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") async def test_resolve_serial_by_id( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test the discovery data resolves to serial/by-id.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] - mock_comports = [ - MagicMock( + mock_ports = [ + USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, @@ -1069,7 +1074,7 @@ async def test_resolve_serial_by_id( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch_scanned_serial_ports(return_value=mock_ports), patch( "homeassistant.components.usb.get_serial_by_id", return_value="/dev/serial/by-id/bla", @@ -1091,73 +1096,73 @@ async def test_resolve_serial_by_id( assert mock_config_flow.mock_calls[0][2]["data"].device == "/dev/serial/by-id/bla" -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") @pytest.mark.parametrize( "ports", [ [ - MagicMock( + USBDevice( device="/dev/cu.usbserial-2120", - vid=0x3039, - pid=0x3039, + vid="3039", + pid="3039", serial_number=conbee_device.serial_number, manufacturer=conbee_device.manufacturer, description=conbee_device.description, ), - MagicMock( + USBDevice( device="/dev/cu.usbserial-1120", - vid=0x3039, - pid=0x3039, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, ), - MagicMock( + USBDevice( device="/dev/cu.SLAB_USBtoUART", - vid=0x3039, - pid=0x3039, + vid="3039", + pid="3039", serial_number=conbee_device.serial_number, manufacturer=conbee_device.manufacturer, description=conbee_device.description, ), - MagicMock( + USBDevice( device="/dev/cu.SLAB_USBtoUART2", - vid=0x3039, - pid=0x3039, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, ), ], [ - MagicMock( + USBDevice( device="/dev/cu.SLAB_USBtoUART2", - vid=0x3039, - pid=0x3039, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, ), - MagicMock( + USBDevice( device="/dev/cu.SLAB_USBtoUART", - vid=0x3039, - pid=0x3039, + vid="3039", + pid="3039", serial_number=conbee_device.serial_number, manufacturer=conbee_device.manufacturer, description=conbee_device.description, ), - MagicMock( + USBDevice( device="/dev/cu.usbserial-1120", - vid=0x3039, - pid=0x3039, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, ), - MagicMock( + USBDevice( device="/dev/cu.usbserial-2120", - vid=0x3039, - pid=0x3039, + vid="3039", + pid="3039", serial_number=conbee_device.serial_number, manufacturer=conbee_device.manufacturer, description=conbee_device.description, @@ -1177,7 +1182,7 @@ async def test_cp2102n_ordering_on_macos( with ( patch("sys.platform", "darwin"), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=ports), + patch_scanned_serial_ports(return_value=ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -1224,34 +1229,31 @@ def test_deprecated_constants( ) -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") @patch("homeassistant.components.usb.REQUEST_SCAN_COOLDOWN", 0) async def test_register_port_event_callback( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test the registration of a port event callback.""" - port1 = Mock( + port1 = USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, ) - port2 = Mock( + port2 = USBDevice( device=conbee_device.device, - vid=12346, - pid=12346, + vid="303A", + pid="303A", serial_number=conbee_device.serial_number, manufacturer=conbee_device.manufacturer, description=conbee_device.description, ) - port1_usb = usb_device_from_port(port1) - port2_usb = usb_device_from_port(port2) - ws_client = await hass_ws_client(hass) mock_callback1 = Mock() @@ -1259,7 +1261,7 @@ async def test_register_port_event_callback( # Start off with no ports with ( - patch("homeassistant.components.usb.comports", return_value=[]), + patch_scanned_serial_ports(return_value=[]), ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -1270,13 +1272,13 @@ async def test_register_port_event_callback( assert mock_callback2.mock_calls == [] # Add two new ports - with patch("homeassistant.components.usb.comports", return_value=[port1, port2]): + with patch_scanned_serial_ports(return_value=[port1, port2]): await ws_client.send_json({"id": 1, "type": "usb/scan"}) response = await ws_client.receive_json() assert response["success"] - assert mock_callback1.mock_calls == [call({port1_usb, port2_usb}, set())] - assert mock_callback2.mock_calls == [call({port1_usb, port2_usb}, set())] + assert mock_callback1.mock_calls == [call({port1, port2}, set())] + assert mock_callback2.mock_calls == [call({port1, port2}, set())] # Cancel the second callback cancel2() @@ -1286,20 +1288,20 @@ async def test_register_port_event_callback( mock_callback2.reset_mock() # Remove port 2 - with patch("homeassistant.components.usb.comports", return_value=[port1]): + with patch_scanned_serial_ports(return_value=[port1]): await ws_client.send_json({"id": 2, "type": "usb/scan"}) response = await ws_client.receive_json() assert response["success"] await hass.async_block_till_done() - assert mock_callback1.mock_calls == [call(set(), {port2_usb})] + assert mock_callback1.mock_calls == [call(set(), {port2})] assert mock_callback2.mock_calls == [] # The second callback was unregistered mock_callback1.reset_mock() mock_callback2.reset_mock() # Keep port 2 removed - with patch("homeassistant.components.usb.comports", return_value=[port1]): + with patch_scanned_serial_ports(return_value=[port1]): await ws_client.send_json({"id": 3, "type": "usb/scan"}) response = await ws_client.receive_json() assert response["success"] @@ -1310,17 +1312,17 @@ async def test_register_port_event_callback( assert mock_callback2.mock_calls == [] # Unplug one and plug in the other - with patch("homeassistant.components.usb.comports", return_value=[port2]): + with patch_scanned_serial_ports(return_value=[port2]): await ws_client.send_json({"id": 4, "type": "usb/scan"}) response = await ws_client.receive_json() assert response["success"] await hass.async_block_till_done() - assert mock_callback1.mock_calls == [call({port2_usb}, {port1_usb})] + assert mock_callback1.mock_calls == [call({port2}, {port1})] assert mock_callback2.mock_calls == [] -@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@pytest.mark.usefixtures("force_usb_polling_watcher") @patch("homeassistant.components.usb.REQUEST_SCAN_COOLDOWN", 0) async def test_register_port_event_callback_failure( hass: HomeAssistant, @@ -1329,27 +1331,24 @@ async def test_register_port_event_callback_failure( ) -> None: """Test port event callback failure handling.""" - port1 = Mock( + port1 = USBDevice( device=slae_sh_device.device, - vid=12345, - pid=12345, + vid="3039", + pid="3039", serial_number=slae_sh_device.serial_number, manufacturer=slae_sh_device.manufacturer, description=slae_sh_device.description, ) - port2 = Mock( + port2 = USBDevice( device=conbee_device.device, - vid=12346, - pid=12346, + vid="303A", + pid="303A", serial_number=conbee_device.serial_number, manufacturer=conbee_device.manufacturer, description=conbee_device.description, ) - port1_usb = usb_device_from_port(port1) - port2_usb = usb_device_from_port(port2) - ws_client = await hass_ws_client(hass) mock_callback1 = Mock(side_effect=RuntimeError("Failure 1")) @@ -1357,7 +1356,7 @@ async def test_register_port_event_callback_failure( # Start off with no ports with ( - patch("homeassistant.components.usb.comports", return_value=[]), + patch_scanned_serial_ports(return_value=[]), ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -1369,7 +1368,7 @@ async def test_register_port_event_callback_failure( # Add two new ports with ( - patch("homeassistant.components.usb.comports", return_value=[port1, port2]), + patch_scanned_serial_ports(return_value=[port1, port2]), caplog.at_level(logging.ERROR, logger="homeassistant.components.usb"), ): await ws_client.send_json({"id": 1, "type": "usb/scan"}) @@ -1378,8 +1377,8 @@ async def test_register_port_event_callback_failure( await hass.async_block_till_done() # Both were called even though they raised exceptions - assert mock_callback1.mock_calls == [call({port1_usb, port2_usb}, set())] - assert mock_callback2.mock_calls == [call({port1_usb, port2_usb}, set())] + assert mock_callback1.mock_calls == [call({port1, port2}, set())] + assert mock_callback2.mock_calls == [call({port1, port2}, set())] assert caplog.text.count("Error in USB port event callback") == 2 assert "Failure 1" in caplog.text From 7592d350a8219124e70da33227385145766cfc7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Mar 2025 20:59:56 -1000 Subject: [PATCH 084/258] Bump aiohomekit to 3.2.13 (#141764) changelog: https://github.com/Jc2k/aiohomekit/compare/3.2.8...3.2.13 --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 98db9a397d3..6562a3edcc9 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.8"], + "requirements": ["aiohomekit==3.2.13"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index fa787fd614c..f015ede6738 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aiohasupervisor==0.3.0 aiohomeconnect==0.16.3 # homeassistant.components.homekit_controller -aiohomekit==3.2.8 +aiohomekit==3.2.13 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08d43ddbcd0..ae5511373ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -252,7 +252,7 @@ aiohasupervisor==0.3.0 aiohomeconnect==0.16.3 # homeassistant.components.homekit_controller -aiohomekit==3.2.8 +aiohomekit==3.2.13 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 From 363bd751291f474732669f636130203e02431c4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Mar 2025 20:59:40 -1000 Subject: [PATCH 085/258] Fix blocking late import of httpcore from httpx (#141771) There is a late import that blocks the event loop in newer version https://github.com/encode/httpx/blob/9e8ab40369bd3ec2cc8bff37ab79bf5769c8b00f/httpx/_transports/default.py#L75 --- homeassistant/helpers/httpx_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index ade2ce747d5..49b12e0aa60 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -7,6 +7,9 @@ import sys from types import TracebackType from typing import Any, Self +# httpx dynamically imports httpcore, so we need to import it +# to avoid it being imported later when the event loop is running +import httpcore # noqa: F401 import httpx from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ From 629c0087f44ac547d7e41c3a930742f41767ad48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Mar 2025 23:40:25 -1000 Subject: [PATCH 086/258] Bump PyISY to 3.1.15 (#141778) changelog: https://github.com/automicus/PyISY/compare/v3.1.14...v3.1.15 fixes #141517 fixes #132279 --- homeassistant/components/isy994/__init__.py | 2 +- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 738c7e2d5ad..e387196ba94 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -138,7 +138,7 @@ async def async_setup_entry( for vtype, _, vid in isy.variables.children: numbers.append(isy.variables[vtype][vid]) if ( - isy.conf[CONFIG_NETWORKING] or isy.conf[CONFIG_PORTAL] + isy.conf[CONFIG_NETWORKING] or isy.conf.get(CONFIG_PORTAL) ) and isy.networking.nobjs: isy_data.devices[CONF_NETWORK] = _create_service_device_info( isy, name=CONFIG_NETWORKING, unique_id=CONF_NETWORK diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 3aa81027b4f..eb804d7af09 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.1.14"], + "requirements": ["pyisy==3.1.15"], "ssdp": [ { "manufacturer": "Universal Devices Inc.", diff --git a/requirements_all.txt b/requirements_all.txt index f015ede6738..42616f85223 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2047,7 +2047,7 @@ pyiskra==0.1.15 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.14 +pyisy==3.1.15 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae5511373ee..625b4b80a93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1668,7 +1668,7 @@ pyiskra==0.1.15 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.14 +pyisy==3.1.15 # homeassistant.components.ituran pyituran==0.1.4 From 6fe158836e1b19f7b94196e83785f24b3e847a38 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 30 Mar 2025 15:03:26 +0200 Subject: [PATCH 087/258] Add boost preset to AVM Fritz!SmartHome climate entities (#141802) * add boost preset to climate entities * add set boost preset test --- homeassistant/components/fritzbox/climate.py | 7 ++- tests/components/fritzbox/test_climate.py | 62 +++++++++++++++++--- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 118e03c391f..57c7e2a696f 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -6,6 +6,7 @@ from typing import Any from homeassistant.components.climate import ( ATTR_HVAC_MODE, + PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, ClimateEntity, @@ -38,7 +39,7 @@ from .sensor import value_scheduled_preset HVAC_MODES = [HVACMode.HEAT, HVACMode.OFF] PRESET_HOLIDAY = "holiday" PRESET_SUMMER = "summer" -PRESET_MODES = [PRESET_ECO, PRESET_COMFORT] +PRESET_MODES = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST] SUPPORTED_FEATURES = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE @@ -194,6 +195,8 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): return PRESET_HOLIDAY if self.data.summer_active: return PRESET_SUMMER + if self.data.target_temperature == ON_API_TEMPERATURE: + return PRESET_BOOST if self.data.target_temperature == self.data.comfort_temperature: return PRESET_COMFORT if self.data.target_temperature == self.data.eco_temperature: @@ -211,6 +214,8 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): await self.async_set_temperature(temperature=self.data.comfort_temperature) elif preset_mode == PRESET_ECO: await self.async_set_temperature(temperature=self.data.eco_temperature) + elif preset_mode == PRESET_BOOST: + await self.async_set_temperature(temperature=ON_REPORT_SET_TEMPERATURE) @property def extra_state_attributes(self) -> ClimateExtraAttributes: diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 0784d7b6188..7766d906f68 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -16,6 +16,7 @@ from homeassistant.components.climate import ( ATTR_PRESET_MODE, ATTR_PRESET_MODES, DOMAIN as CLIMATE_DOMAIN, + PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, SERVICE_SET_HVAC_MODE, @@ -80,7 +81,11 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: assert state.attributes[ATTR_MAX_TEMP] == 28 assert state.attributes[ATTR_MIN_TEMP] == 8 assert state.attributes[ATTR_PRESET_MODE] is None - assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] + assert state.attributes[ATTR_PRESET_MODES] == [ + PRESET_ECO, + PRESET_COMFORT, + PRESET_BOOST, + ] assert state.attributes[ATTR_STATE_BATTERY_LOW] is True assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False assert state.attributes[ATTR_STATE_SUMMER_MODE] is False @@ -434,11 +439,31 @@ async def test_set_preset_mode_eco( assert device.set_target_temperature.call_args_list == expected_call_args +async def test_set_preset_mode_boost( + hass: HomeAssistant, + fritz: Mock, +) -> None: + """Test setting preset mode.""" + device = FritzDeviceClimateMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_BOOST}, + True, + ) + assert device.set_target_temperature.call_count == 1 + assert device.set_target_temperature.call_args_list == [call(30, True)] + + async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: """Test preset mode.""" device = FritzDeviceClimateMock() - device.comfort_temperature = 98 - device.eco_temperature = 99 + device.comfort_temperature = 23 + device.eco_temperature = 20 assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -447,8 +472,8 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: assert state assert state.attributes[ATTR_PRESET_MODE] is None - device.target_temperature = 98 - + # test comfort preset + device.target_temperature = 23 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done(wait_background_tasks=True) @@ -458,8 +483,8 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: assert state assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT - device.target_temperature = 99 - + # test eco preset + device.target_temperature = 20 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done(wait_background_tasks=True) @@ -469,6 +494,17 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: assert state assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + # test boost preset + device.target_temperature = 127 # special temp from the api + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get(ENTITY_ID) + + assert fritz().update_devices.call_count == 4 + assert state + assert state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST + async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: """Test adding new discovered devices during runtime.""" @@ -509,7 +545,11 @@ async def test_holidy_summer_mode( assert state.attributes[ATTR_STATE_SUMMER_MODE] is False assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT, HVACMode.OFF] assert state.attributes[ATTR_PRESET_MODE] is None - assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] + assert state.attributes[ATTR_PRESET_MODES] == [ + PRESET_ECO, + PRESET_COMFORT, + PRESET_BOOST, + ] # test holiday mode device.holiday_active = True @@ -596,4 +636,8 @@ async def test_holidy_summer_mode( assert state.attributes[ATTR_STATE_SUMMER_MODE] is False assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT, HVACMode.OFF] assert state.attributes[ATTR_PRESET_MODE] is None - assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] + assert state.attributes[ATTR_PRESET_MODES] == [ + PRESET_ECO, + PRESET_COMFORT, + PRESET_BOOST, + ] From d4640f1d24b1937ad0b24c94b8be4ca2773bc58a Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:58:47 +0200 Subject: [PATCH 088/258] Bump ical to 9.0.3 (#141805) --- 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 81fd2b07de4..efce97a0d6f 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.0.1"] + "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index fc6d0bc00c7..528552aaa57 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.0.1"] + "requirements": ["ical==9.0.3"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 27d3ccce4a7..6f117131c20 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.0.1"] + "requirements": ["ical==9.0.3"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index fe17a3d2c34..256f5baf0ff 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.0.1"] + "requirements": ["ical==9.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 42616f85223..7c89c2ef9c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1196,7 +1196,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.0.1 +ical==9.0.3 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 625b4b80a93..8850cc9001a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1014,7 +1014,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.0.1 +ical==9.0.3 # homeassistant.components.caldav icalendar==6.1.0 From fc0d71e891d11a9f597cba3a0543cdbe3fc3596f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 30 Mar 2025 13:57:53 +0100 Subject: [PATCH 089/258] Fix System Bridge wait timeout wait condition (#141811) * Fix System Bridge wait timeout wait condition * Add DataMissingException as a timeout condition * Add tests --- .../components/system_bridge/__init__.py | 3 +- .../components/system_bridge/const.py | 4 +- .../components/system_bridge/coordinator.py | 7 ++- tests/components/system_bridge/test_init.py | 50 +++++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 3bda29867cc..e1ee57e42b2 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -11,6 +11,7 @@ from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, + DataMissingException, ) from systembridgeconnector.version import Version from systembridgemodels.keyboard_key import KeyboardKey @@ -184,7 +185,7 @@ async def async_setup_entry( "host": entry.data[CONF_HOST], }, ) from exception - except TimeoutError as exception: + except (DataMissingException, TimeoutError) as exception: raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="timeout", diff --git a/homeassistant/components/system_bridge/const.py b/homeassistant/components/system_bridge/const.py index 32507f6d84e..235d7e6b986 100644 --- a/homeassistant/components/system_bridge/const.py +++ b/homeassistant/components/system_bridge/const.py @@ -18,4 +18,6 @@ MODULES: Final[list[Module]] = [ Module.SYSTEM, ] -DATA_WAIT_TIMEOUT: Final[int] = 10 +DATA_WAIT_TIMEOUT: Final[int] = 20 + +GET_DATA_WAIT_TIMEOUT: Final[int] = 15 diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 1690bad4a4d..7e545f39e46 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -33,7 +33,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, MODULES +from .const import DOMAIN, GET_DATA_WAIT_TIMEOUT, MODULES from .data import SystemBridgeData @@ -119,7 +119,10 @@ class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[SystemBridgeData]) """Get data from WebSocket.""" await self.check_websocket_connected() - modules_data = await self.websocket_client.get_data(GetData(modules=modules)) + modules_data = await self.websocket_client.get_data( + GetData(modules=modules), + timeout=GET_DATA_WAIT_TIMEOUT, + ) # Merge new data with existing data for module in MODULES: diff --git a/tests/components/system_bridge/test_init.py b/tests/components/system_bridge/test_init.py index 7632a0c8157..25ccbdeb46c 100644 --- a/tests/components/system_bridge/test_init.py +++ b/tests/components/system_bridge/test_init.py @@ -81,3 +81,53 @@ async def test_migration_minor_future_version(hass: HomeAssistant) -> None: assert config_entry.minor_version == config_entry_minor_version assert config_entry.data == config_entry_data assert config_entry.state is ConfigEntryState.LOADED + + +async def test_setup_timeout(hass: HomeAssistant) -> None: + """Test setup with timeout error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_UUID, + data=FIXTURE_USER_INPUT, + version=SystemBridgeConfigFlow.VERSION, + minor_version=SystemBridgeConfigFlow.MINOR_VERSION, + ) + + with patch( + "systembridgeconnector.version.Version.check_supported", + side_effect=TimeoutError, + ): + config_entry.add_to_hass(hass) + result = await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert result is False + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_coordinator_get_data_timeout(hass: HomeAssistant) -> None: + """Test coordinator handling timeout during get_data.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_UUID, + data=FIXTURE_USER_INPUT, + version=SystemBridgeConfigFlow.VERSION, + minor_version=SystemBridgeConfigFlow.MINOR_VERSION, + ) + + with ( + patch( + "systembridgeconnector.version.Version.check_supported", + return_value=True, + ), + patch( + "homeassistant.components.system_bridge.coordinator.SystemBridgeDataUpdateCoordinator.async_get_data", + side_effect=TimeoutError, + ), + ): + config_entry.add_to_hass(hass) + result = await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert result is False + assert config_entry.state is ConfigEntryState.SETUP_RETRY From a592feae3d6e6ff698b43154268afc677198e967 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 30 Mar 2025 15:01:06 +0200 Subject: [PATCH 090/258] Correct spelling for 'availability` in MQTT translation strings (#141818) --- 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 2bc8ff3b71f..cedf120def1 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -126,7 +126,7 @@ "payload_not_available": "Payload not available" }, "data_description": { - "availability_topic": "Topic to receive the availabillity payload on", + "availability_topic": "Topic to receive the availability payload on", "availability_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-templates-with-the-mqtt-integration) to render the availability payload received on the availability topic", "payload_available": "The payload that indicates the device is available (defaults to 'online')", "payload_not_available": "The payload that indicates the device is not available (defaults to 'offline')" From e66416c23d75dd71e2e0cd1e2fe3540bc442e5dd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Mar 2025 17:44:48 +0200 Subject: [PATCH 091/258] Fix hardcoded UoM for total power sensor for Tuya zndb devices (#141822) --- homeassistant/components/tuya/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 29bdffe1c28..9e40bda5d4d 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -832,7 +832,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { translation_key="total_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfPower.KILO_WATT, subkey="power", ), TuyaSensorEntityDescription( From 3aa56936adf07906967499b02929d83b830b7fd1 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:49:41 +0200 Subject: [PATCH 092/258] Move setup messages from info to debug level (#141834) move info to debug level --- homeassistant/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 9572136559a..334e3a9e074 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -387,7 +387,7 @@ async def _async_setup_component( }, ) - _LOGGER.info("Setting up %s", domain) + _LOGGER.debug("Setting up %s", domain) with async_start_setup(hass, integration=domain, phase=SetupPhases.SETUP): if hasattr(component, "PLATFORM_SCHEMA"): @@ -783,7 +783,7 @@ def async_start_setup( # platforms, but we only care about the longest time. group_setup_times[phase] = max(group_setup_times[phase], time_taken) if group is None: - _LOGGER.info( + _LOGGER.debug( "Setup of domain %s took %.2f seconds", integration, time_taken ) elif _LOGGER.isEnabledFor(logging.DEBUG): From 7773cc121efa8182cc43d359bff0e795f1d54efa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 30 Mar 2025 17:46:03 +0200 Subject: [PATCH 093/258] Fix the entity category for max throughput sensors in AVM Fritz!Box Tools (#141838) correct the entity category for max throughput sensors --- homeassistant/components/fritz/sensor.py | 4 ++-- tests/components/fritz/snapshots/test_sensor.ambr | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index bcee590460f..88de9ebdefc 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -193,7 +193,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( translation_key="max_kb_s_sent", native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, - entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_max_kb_s_sent_state, ), FritzSensorEntityDescription( @@ -201,7 +200,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( translation_key="max_kb_s_received", native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, - entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_max_kb_s_received_state, ), FritzSensorEntityDescription( @@ -225,6 +223,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( translation_key="link_kb_s_sent", native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_link_kb_s_sent_state, ), FritzSensorEntityDescription( @@ -232,6 +231,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( translation_key="link_kb_s_received", native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_link_kb_s_received_state, ), FritzSensorEntityDescription( diff --git a/tests/components/fritz/snapshots/test_sensor.ambr b/tests/components/fritz/snapshots/test_sensor.ambr index 5ff0e448b15..ffede386099 100644 --- a/tests/components/fritz/snapshots/test_sensor.ambr +++ b/tests/components/fritz/snapshots/test_sensor.ambr @@ -453,7 +453,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.mock_title_link_download_throughput', 'has_entity_name': True, 'hidden_by': None, @@ -598,7 +598,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.mock_title_link_upload_throughput', 'has_entity_name': True, 'hidden_by': None, @@ -647,7 +647,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.mock_title_max_connection_download_throughput', 'has_entity_name': True, 'hidden_by': None, @@ -696,7 +696,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.mock_title_max_connection_upload_throughput', 'has_entity_name': True, 'hidden_by': None, From 69753fca1d0163878eb5562633207bc50c267365 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Mar 2025 20:12:42 +0200 Subject: [PATCH 094/258] Update pvo to v2.2.1 (#141847) --- homeassistant/components/pvoutput/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 9dbdad53bcb..dee5f9cda6e 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/pvoutput", "integration_type": "device", "iot_class": "cloud_polling", - "requirements": ["pvo==2.2.0"] + "requirements": ["pvo==2.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7c89c2ef9c9..1365136bbe8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1722,7 +1722,7 @@ pushbullet.py==0.11.0 pushover_complete==1.1.1 # homeassistant.components.pvoutput -pvo==2.2.0 +pvo==2.2.1 # homeassistant.components.aosmith py-aosmith==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8850cc9001a..737c77919d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1421,7 +1421,7 @@ pushbullet.py==0.11.0 pushover_complete==1.1.1 # homeassistant.components.pvoutput -pvo==2.2.0 +pvo==2.2.1 # homeassistant.components.aosmith py-aosmith==1.0.12 From 9e81ec5aae94f827a9cd4bbac5a11ed6d1285629 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Mar 2025 15:25:24 -1000 Subject: [PATCH 095/258] Handle encryption being disabled on an ESPHome device (#141887) fixes #121442 --- .../components/esphome/config_flow.py | 15 ++++++++++ homeassistant/components/esphome/manager.py | 2 ++ homeassistant/components/esphome/strings.json | 3 ++ tests/components/esphome/test_config_flow.py | 30 +++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 955a93cd2b7..686d77d9b34 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -128,8 +128,23 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._password = "" return await self._async_authenticate_or_add() + if error is None and entry_data.get(CONF_NOISE_PSK): + return await self.async_step_reauth_encryption_removed_confirm() return await self.async_step_reauth_confirm() + async def async_step_reauth_encryption_removed_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauthorization flow when encryption was removed.""" + if user_input is not None: + self._noise_psk = None + return self._async_get_entry() + + return self.async_show_form( + step_id="reauth_encryption_removed_confirm", + description_placeholders={"name": self._name}, + ) + async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 0a47fb66815..7ce96a0f510 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -13,6 +13,7 @@ from aioesphomeapi import ( APIConnectionError, APIVersion, DeviceInfo as EsphomeDeviceInfo, + EncryptionHelloAPIError, EntityInfo, HomeassistantServiceCall, InvalidAuthAPIError, @@ -570,6 +571,7 @@ class ESPHomeManager: if isinstance( err, ( + EncryptionHelloAPIError, RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError, InvalidAuthAPIError, diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index c6916a3636d..437b9ac2098 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -43,6 +43,9 @@ }, "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration." }, + "reauth_encryption_removed_confirm": { + "description": "The ESPHome device {name} disabled transport encryption. Please confirm that you want to remove the encryption key and allow unencrypted connections." + }, "discovery_confirm": { "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", "title": "Discovered ESPHome node" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index afca6f76b43..d48a1f40482 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1047,6 +1047,36 @@ async def test_reauth_confirm_invalid_with_unique_id( assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") +async def test_reauth_encryption_key_removed( + hass: HomeAssistant, mock_client, mock_setup_entry: None +) -> None: + """Test reauth when the encryption key was removed.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + }, + unique_id="test", + ) + entry.add_to_hass(hass) + + result = await entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_encryption_removed_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == "" + + async def test_discovery_dhcp_updates_host( hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None ) -> None: From 627f994872140e3a4b7e5c6360453a8ce6c4ddc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Mar 2025 15:14:17 -1000 Subject: [PATCH 096/258] Bump aioesphomeapi to 29.8.0 (#141888) changelog: https://github.com/esphome/aioesphomeapi/compare/v29.7.0...v29.8.0 --- 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 075185dffbb..954968f5e2c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], "requirements": [ - "aioesphomeapi==29.7.0", + "aioesphomeapi==29.8.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==2.12.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 1365136bbe8..6d9afb7c5e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==29.7.0 +aioesphomeapi==29.8.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 737c77919d4..fc90fec6d06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==29.7.0 +aioesphomeapi==29.8.0 # homeassistant.components.flo aioflo==2021.11.0 From 06aaf188eaf55213cc660c43927cbfba7703fc0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Mar 2025 15:23:54 -1000 Subject: [PATCH 097/258] Fix duplicate call to async_write_ha_state when adding elkm1 entities (#141890) When an entity is added state is always written in add_to_platform_finish: https://github.com/home-assistant/core/blob/7336178e03a80be11f54eadd6833b9a2a40bae30/homeassistant/helpers/entity.py#L1384 We should not do it in async_added_to_hass as well --- homeassistant/components/elkm1/entity.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/elkm1/entity.py b/homeassistant/components/elkm1/entity.py index d9967d93967..ce717578eae 100644 --- a/homeassistant/components/elkm1/entity.py +++ b/homeassistant/components/elkm1/entity.py @@ -100,7 +100,11 @@ class ElkEntity(Entity): return {"index": self._element.index + 1} def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None: - pass + """Handle changes to the element. + + This method is called when the element changes. It should be + overridden by subclasses to handle the changes. + """ @callback def _element_callback(self, element: Element, changeset: dict[str, Any]) -> None: @@ -111,7 +115,7 @@ class ElkEntity(Entity): async def async_added_to_hass(self) -> None: """Register callback for ElkM1 changes and update entity state.""" self._element.add_callback(self._element_callback) - self._element_callback(self._element, {}) + self._element_changed(self._element, {}) @property def device_info(self) -> DeviceInfo: From 7f9e4ba39ef25d3ad6baaf4ae66e48461502c64b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Mar 2025 23:31:45 -0400 Subject: [PATCH 098/258] Ensure user always has first turn for Google Gen AI (#141893) --- .../conversation.py | 9 ++++ .../test_conversation.py | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 5460f48f20e..7c19c5445a7 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -356,6 +356,15 @@ class GoogleGenerativeAIConversationEntity( messages.append(_convert_content(chat_content)) + # The SDK requires the first message to be a user message + # This is not the case if user used `start_conversation` + # Workaround from https://github.com/googleapis/python-genai/issues/529#issuecomment-2740964537 + if messages and messages[0].role != "user": + messages.insert( + 0, + Content(role="user", parts=[Part.from_text(text=" ")]), + ) + if tool_results: messages.append(_create_google_tool_response_content(tool_results)) generateContentConfig = GenerateContentConfig( diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index a2b238b9399..9c4ecc4f9a4 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -715,3 +715,48 @@ async def test_empty_content_in_chat_history( 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, +) -> 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?", + ) + ) + + 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=[]))] + + await conversation.async_converse( + hass, + "hello", + chat_log.conversation_id, + Context(), + agent_id="conversation.google_generative_ai_conversation", + ) + + _, kwargs = mock_create.call_args + actual_history = kwargs.get("history") + + assert actual_history[0].parts[0].text == " " + assert actual_history[0].role == "user" + assert ( + actual_history[1].parts[0].text + == "Garage door left open, do you want to close it?" + ) + assert actual_history[1].role == "model" From 04f5315ab24f345318062a27d1ebf10a20f44430 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Mar 2025 08:09:39 +0000 Subject: [PATCH 099/258] Bump version to 2025.4.0b10 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d3282d068e..ea9b238e310 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 = 4 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0b10" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0) diff --git a/pyproject.toml b/pyproject.toml index 75aaf797fae..97dda40bf6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b9" +version = "2025.4.0b10" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From d0b61af7ecdd9543d851cf8ea5ad2f12e31f712d Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:42:31 +0800 Subject: [PATCH 100/258] Add switchbot cover unit tests (#140265) * add cover unit tests * Add unit test for SwitchBot cover * fix: use mock_restore_cache to mock the last state * modify unit tests * modify scripts as suggest * improve readability * adjust patch target per review comments * adjust patch target per review comments --------- Co-authored-by: J. Nick Koston --- homeassistant/components/switchbot/cover.py | 2 +- tests/components/switchbot/__init__.py | 67 ++++ tests/components/switchbot/conftest.py | 19 ++ tests/components/switchbot/test_cover.py | 327 ++++++++++++++++++++ 4 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 tests/components/switchbot/test_cover.py diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 3ef0f5625c2..5a9613ab2a2 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -154,7 +154,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity): ATTR_CURRENT_TILT_POSITION ) self._last_run_success = last_state.attributes.get("last_run_success") - if (_tilt := self._attr_current_cover_position) is not None: + if (_tilt := self._attr_current_cover_tilt_position) is not None: self._attr_is_closed = (_tilt < self.CLOSED_DOWN_THRESHOLD) or ( _tilt > self.CLOSED_UP_THRESHOLD ) diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index d123c93a873..715073aa891 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -319,3 +319,70 @@ WOHUB2_SERVICE_INFO = BluetoothServiceInfoBleak( connectable=True, tx_power=-127, ) + + +WOCURTAIN3_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoCurtain3", + address="AA:BB:CC:DD:EE:FF", + manufacturer_data={2409: b"\xcf;Zwu\x0c\x19\x0b\x00\x11D\x006"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"{\xc06\x00\x11D"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="WoCurtain3", + manufacturer_data={2409: b"\xcf;Zwu\x0c\x19\x0b\x00\x11D\x006"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"{\xc06\x00\x11D"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "WoCurtain3"), + time=0, + connectable=True, + tx_power=-127, +) + + +WOBLINDTILT_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoBlindTilt", + address="AA:BB:CC:DD:EE:FF", + manufacturer_data={2409: b"\xfbgA`\x98\xe8\x1d%2\x11\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"x\x00*"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="WoBlindTilt", + manufacturer_data={2409: b"\xfbgA`\x98\xe8\x1d%2\x11\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"x\x00*"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "WoBlindTilt"), + time=0, + connectable=True, + tx_power=-127, +) + + +def make_advertisement( + address: str, manufacturer_data: bytes, service_data: bytes +) -> BluetoothServiceInfoBleak: + """Make a dummy advertisement.""" + return BluetoothServiceInfoBleak( + name="Test Device", + address=address, + manufacturer_data={2409: manufacturer_data}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": service_data}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Test Device", + manufacturer_data={2409: manufacturer_data}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": service_data}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device(address, "Test Device"), + time=0, + connectable=True, + tx_power=-127, + ) diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 44f68a1c8ae..aff94626a68 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -2,7 +2,26 @@ import pytest +from homeassistant.components.switchbot.const import DOMAIN +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_SENSOR_TYPE + +from tests.common import MockConfigEntry + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" + + +@pytest.fixture +def mock_entry_factory(): + """Fixture to create a MockConfigEntry with a customizable sensor type.""" + return lambda sensor_type="curtain": MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_SENSOR_TYPE: sensor_type, + }, + unique_id="aabbccddeeff", + ) diff --git a/tests/components/switchbot/test_cover.py b/tests/components/switchbot/test_cover.py new file mode 100644 index 00000000000..8810963f63d --- /dev/null +++ b/tests/components/switchbot/test_cover.py @@ -0,0 +1,327 @@ +"""Test the switchbot covers.""" + +from collections.abc import Callable +from unittest.mock import AsyncMock, patch + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, + DOMAIN as COVER_DOMAIN, + CoverState, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, +) +from homeassistant.core import HomeAssistant, State + +from . import WOBLINDTILT_SERVICE_INFO, WOCURTAIN3_SERVICE_INFO, make_advertisement + +from tests.common import MockConfigEntry, mock_restore_cache +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_curtain3_setup( + hass: HomeAssistant, mock_entry_factory: Callable[[str], MockConfigEntry] +) -> None: + """Test setting up the Curtain3.""" + inject_bluetooth_service_info(hass, WOCURTAIN3_SERVICE_INFO) + + entry = mock_entry_factory(sensor_type="curtain") + + entity_id = "cover.test_name" + mock_restore_cache( + hass, + [ + State( + entity_id, + CoverState.OPEN, + {ATTR_CURRENT_POSITION: 50}, + ) + ], + ) + + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_POSITION] == 50 + + +async def test_curtain3_controlling( + hass: HomeAssistant, mock_entry_factory: Callable[[str], MockConfigEntry] +) -> None: + """Test Curtain3 controlling.""" + inject_bluetooth_service_info(hass, WOCURTAIN3_SERVICE_INFO) + + entry = mock_entry_factory(sensor_type="curtain") + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotCurtain.open", + new=AsyncMock(return_value=True), + ) as mock_open, + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotCurtain.close", + new=AsyncMock(return_value=True), + ) as mock_close, + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotCurtain.stop", + new=AsyncMock(return_value=True), + ) as mock_stop, + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotCurtain.set_position", + new=AsyncMock(return_value=True), + ) as mock_set_position, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_id = "cover.test_name" + address = "AA:BB:CC:DD:EE:FF" + service_data = b"{\xc06\x00\x11D" + + # Test open + manufacturer_data = b"\xcf;Zwu\x0c\x19\x0b\x05\x11D\x006" + await hass.services.async_call( + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + inject_bluetooth_service_info( + hass, make_advertisement(address, manufacturer_data, service_data) + ) + await hass.async_block_till_done() + + mock_open.assert_awaited_once() + state = hass.states.get(entity_id) + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_POSITION] == 95 + + # Test close + manufacturer_data = b"\xcf;Zwu\x0c\x19\x0b\x58\x11D\x006" + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + inject_bluetooth_service_info( + hass, make_advertisement(address, manufacturer_data, service_data) + ) + await hass.async_block_till_done() + + mock_close.assert_awaited_once() + state = hass.states.get(entity_id) + assert state.state == CoverState.CLOSED + assert state.attributes[ATTR_CURRENT_POSITION] == 12 + + # Test stop + manufacturer_data = b"\xcf;Zwu\x0c\x19\x0b\x3c\x11D\x006" + await hass.services.async_call( + COVER_DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + inject_bluetooth_service_info( + hass, make_advertisement(address, manufacturer_data, service_data) + ) + await hass.async_block_till_done() + + mock_stop.assert_awaited_once() + state = hass.states.get(entity_id) + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_POSITION] == 40 + + # Test set position + manufacturer_data = b"\xcf;Zwu\x0c\x19\x0b(\x11D\x006" + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50}, + blocking=True, + ) + inject_bluetooth_service_info( + hass, make_advertisement(address, manufacturer_data, service_data) + ) + await hass.async_block_till_done() + + mock_set_position.assert_awaited_once() + state = hass.states.get(entity_id) + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_POSITION] == 60 + + +async def test_blindtilt_setup( + hass: HomeAssistant, mock_entry_factory: Callable[[str], MockConfigEntry] +) -> None: + """Test setting up the blindtilt.""" + inject_bluetooth_service_info(hass, WOBLINDTILT_SERVICE_INFO) + + entry = mock_entry_factory(sensor_type="blind_tilt") + entity_id = "cover.test_name" + mock_restore_cache( + hass, + [ + State( + entity_id, + CoverState.OPEN, + {ATTR_CURRENT_TILT_POSITION: 40}, + ) + ], + ) + + entry.add_to_hass(hass) + with patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.update", + new=AsyncMock(return_value=True), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 40 + + +async def test_blindtilt_controlling( + hass: HomeAssistant, mock_entry_factory: Callable[[str], MockConfigEntry] +) -> None: + """Test blindtilt controlling.""" + inject_bluetooth_service_info(hass, WOBLINDTILT_SERVICE_INFO) + + entry = mock_entry_factory(sensor_type="blind_tilt") + entry.add_to_hass(hass) + info = { + "motionDirection": { + "opening": False, + "closing": False, + "up": False, + "down": False, + }, + } + with ( + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.get_basic_info", + new=AsyncMock(return_value=info), + ), + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.open", + new=AsyncMock(return_value=True), + ) as mock_open, + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.close", + new=AsyncMock(return_value=True), + ) as mock_close, + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.stop", + new=AsyncMock(return_value=True), + ) as mock_stop, + patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.set_position", + new=AsyncMock(return_value=True), + ) as mock_set_position, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_id = "cover.test_name" + address = "AA:BB:CC:DD:EE:FF" + service_data = b"x\x00*" + + # Test open + manufacturer_data = b"\xfbgA`\x98\xe8\x1d%F\x12\x85" + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + with patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.get_basic_info", + return_value=info, + ): + inject_bluetooth_service_info( + hass, make_advertisement(address, manufacturer_data, service_data) + ) + await hass.async_block_till_done() + + mock_open.assert_awaited_once() + + state = hass.states.get(entity_id) + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 70 + + # Test close + manufacturer_data = b"\xfbgA`\x98\xe8\x1d%\x0f\x12\x85" + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + with patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.get_basic_info", + return_value=info, + ): + inject_bluetooth_service_info( + hass, make_advertisement(address, manufacturer_data, service_data) + ) + await hass.async_block_till_done() + + mock_close.assert_awaited_once() + state = hass.states.get(entity_id) + assert state.state == CoverState.CLOSED + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 15 + + # Test stop + manufacturer_data = b"\xfbgA`\x98\xe8\x1d%\n\x12\x85" + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + with patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.get_basic_info", + return_value=info, + ): + inject_bluetooth_service_info( + hass, make_advertisement(address, manufacturer_data, service_data) + ) + await hass.async_block_till_done() + + mock_stop.assert_awaited_once() + state = hass.states.get(entity_id) + assert state.state == CoverState.CLOSED + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 10 + + # Test set position + manufacturer_data = b"\xfbgA`\x98\xe8\x1d%2\x12\x85" + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: entity_id, ATTR_TILT_POSITION: 50}, + blocking=True, + ) + with patch( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotBlindTilt.get_basic_info", + return_value=info, + ): + inject_bluetooth_service_info( + hass, make_advertisement(address, manufacturer_data, service_data) + ) + await hass.async_block_till_done() + + mock_set_position.assert_awaited_once() + state = hass.states.get(entity_id) + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 From a88678cf42163d7f3673f1643f9395f19efb2e31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Mar 2025 11:41:52 +0200 Subject: [PATCH 101/258] Fix SmartThings climate entity missing off HAVC mode (#141700) * Fix smartthing climate entity missing off HAVC mode: * Fix tests * Fix test --------- Co-authored-by: Joostlek --- homeassistant/components/smartthings/climate.py | 2 +- tests/components/smartthings/snapshots/test_climate.ambr | 8 ++++++++ tests/components/smartthings/test_climate.py | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index e20f191352f..9f94293d863 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -281,7 +281,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): return [ state for mode in supported_thermostat_modes - if (state := AC_MODE_TO_STATE.get(mode)) is not None + if (state := MODE_TO_STATE.get(mode)) is not None ] @property diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index 10e9dbd5489..17e25421fec 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -70,6 +70,7 @@ 'area_id': None, 'capabilities': dict({ 'hvac_modes': list([ + , , ]), 'max_temp': 35, @@ -109,6 +110,7 @@ 'current_temperature': 23.9, 'friendly_name': 'Radiator Thermostat II [+M] Wohnzimmer', 'hvac_modes': list([ + , , ]), 'max_temp': 35, @@ -431,6 +433,7 @@ 'auto', ]), 'hvac_modes': list([ + , , , ]), @@ -478,6 +481,7 @@ 'friendly_name': 'Main Floor', 'hvac_action': , 'hvac_modes': list([ + , , , ]), @@ -628,6 +632,7 @@ 'area_id': None, 'capabilities': dict({ 'hvac_modes': list([ + , , ]), 'max_temp': 35, @@ -668,6 +673,7 @@ 'friendly_name': 'Hall thermostat', 'hvac_action': , 'hvac_modes': list([ + , , ]), 'max_temp': 35, @@ -695,6 +701,7 @@ 'on', ]), 'hvac_modes': list([ + , ]), 'max_temp': 35.0, 'min_temp': 7.0, @@ -738,6 +745,7 @@ 'friendly_name': 'asd', 'hvac_action': , '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 380c4072860..75b864598bd 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -817,10 +817,10 @@ async def test_updating_humidity( ( Capability.THERMOSTAT_MODE, Attribute.SUPPORTED_THERMOSTAT_MODES, - ["coolClean", "dryClean"], + ["rush hour", "heat"], ATTR_HVAC_MODES, - [], - [HVACMode.COOL, HVACMode.DRY], + [HVACMode.AUTO], + [HVACMode.AUTO, HVACMode.HEAT], ), ], ids=[ From 506526a6a2caad25167377d21fd884b04cb14858 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:40:14 +0200 Subject: [PATCH 102/258] Handle 403 error in remote calendar (#141839) * Handle 403 error in remote calendar * tests --- .../components/remote_calendar/config_flow.py | 8 ++++++++ .../components/remote_calendar/strings.json | 1 + .../remote_calendar/test_config_flow.py | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/remote_calendar/config_flow.py b/homeassistant/components/remote_calendar/config_flow.py index 1ceeb7a3937..cc9f45e2767 100644 --- a/homeassistant/components/remote_calendar/config_flow.py +++ b/homeassistant/components/remote_calendar/config_flow.py @@ -1,5 +1,6 @@ """Config flow for Remote Calendar integration.""" +from http import HTTPStatus import logging from typing import Any @@ -50,6 +51,13 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN): client = get_async_client(self.hass) try: res = await client.get(user_input[CONF_URL], follow_redirects=True) + if res.status_code == HTTPStatus.FORBIDDEN: + errors["base"] = "forbidden" + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + ) res.raise_for_status() except (HTTPError, InvalidURL) as err: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/remote_calendar/strings.json b/homeassistant/components/remote_calendar/strings.json index 1ad62821818..fff2d4abbb3 100644 --- a/homeassistant/components/remote_calendar/strings.json +++ b/homeassistant/components/remote_calendar/strings.json @@ -19,6 +19,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "forbidden": "The server understood the request but refuses to authorize it.", "invalid_ics_file": "[%key:component::local_calendar::config::error::invalid_ics_file%]" } }, diff --git a/tests/components/remote_calendar/test_config_flow.py b/tests/components/remote_calendar/test_config_flow.py index 9eb9cb40134..9aff1594db3 100644 --- a/tests/components/remote_calendar/test_config_flow.py +++ b/tests/components/remote_calendar/test_config_flow.py @@ -165,8 +165,17 @@ async def test_unsupported_inputs( ## and then the exception isn't raised anymore. +@pytest.mark.parametrize( + ("http_status", "error"), + [ + (401, "cannot_connect"), + (403, "forbidden"), + ], +) @respx.mock -async def test_form_http_status_error(hass: HomeAssistant, ics_content: str) -> None: +async def test_form_http_status_error( + hass: HomeAssistant, ics_content: str, http_status: int, error: str +) -> None: """Test we http status.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -174,7 +183,7 @@ async def test_form_http_status_error(hass: HomeAssistant, ics_content: str) -> assert result["type"] is FlowResultType.FORM respx.get(CALENDER_URL).mock( return_value=Response( - status_code=403, + status_code=http_status, ) ) @@ -186,7 +195,7 @@ async def test_form_http_status_error(hass: HomeAssistant, ics_content: str) -> }, ) assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": error} respx.get(CALENDER_URL).mock( return_value=Response( status_code=200, From e16ba27ce86ae9dfd133f4652979e77331e15741 Mon Sep 17 00:00:00 2001 From: Dan Raper Date: Mon, 31 Mar 2025 10:44:01 +0100 Subject: [PATCH 103/258] Bump ohmepy to 1.5.1 (#141879) * Bump ohmepy to 1.5.1 * Fix types for ohmepy version change --- homeassistant/components/ohme/button.py | 5 +++-- homeassistant/components/ohme/manifest.json | 2 +- homeassistant/components/ohme/number.py | 9 +++++---- homeassistant/components/ohme/select.py | 4 ++-- homeassistant/components/ohme/sensor.py | 4 ++-- homeassistant/components/ohme/services.py | 2 +- homeassistant/components/ohme/time.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ohme/test_services.py | 12 +++++++----- 10 files changed, 26 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ohme/button.py b/homeassistant/components/ohme/button.py index 6e942215c0f..41782ea4a2d 100644 --- a/homeassistant/components/ohme/button.py +++ b/homeassistant/components/ohme/button.py @@ -2,8 +2,9 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass +from typing import Any from ohme import ApiException, ChargerStatus, OhmeApiClient @@ -23,7 +24,7 @@ PARALLEL_UPDATES = 1 class OhmeButtonDescription(OhmeEntityDescription, ButtonEntityDescription): """Class describing Ohme button entities.""" - press_fn: Callable[[OhmeApiClient], Awaitable[None]] + press_fn: Callable[[OhmeApiClient], Coroutine[Any, Any, bool]] BUTTON_DESCRIPTIONS = [ diff --git a/homeassistant/components/ohme/manifest.json b/homeassistant/components/ohme/manifest.json index f0021808d92..30a55360ce2 100644 --- a/homeassistant/components/ohme/manifest.json +++ b/homeassistant/components/ohme/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["ohme==1.4.1"] + "requirements": ["ohme==1.5.1"] } diff --git a/homeassistant/components/ohme/number.py b/homeassistant/components/ohme/number.py index 0c71bab009f..f412c658085 100644 --- a/homeassistant/components/ohme/number.py +++ b/homeassistant/components/ohme/number.py @@ -1,7 +1,8 @@ """Platform for number.""" -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass +from typing import Any from ohme import ApiException, OhmeApiClient @@ -22,7 +23,7 @@ PARALLEL_UPDATES = 1 class OhmeNumberDescription(OhmeEntityDescription, NumberEntityDescription): """Class describing Ohme number entities.""" - set_fn: Callable[[OhmeApiClient, float], Awaitable[None]] + set_fn: Callable[[OhmeApiClient, float], Coroutine[Any, Any, bool]] value_fn: Callable[[OhmeApiClient], float] @@ -31,7 +32,7 @@ NUMBER_DESCRIPTION = [ key="target_percentage", translation_key="target_percentage", value_fn=lambda client: client.target_soc, - set_fn=lambda client, value: client.async_set_target(target_percent=value), + set_fn=lambda client, value: client.async_set_target(target_percent=int(value)), native_min_value=0, native_max_value=100, native_step=1, @@ -42,7 +43,7 @@ NUMBER_DESCRIPTION = [ translation_key="preconditioning_duration", value_fn=lambda client: client.preconditioning, set_fn=lambda client, value: client.async_set_target( - pre_condition_length=value + pre_condition_length=int(value) ), native_min_value=0, native_max_value=60, diff --git a/homeassistant/components/ohme/select.py b/homeassistant/components/ohme/select.py index f065afeb176..d8d9c52c3b6 100644 --- a/homeassistant/components/ohme/select.py +++ b/homeassistant/components/ohme/select.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any, Final @@ -24,7 +24,7 @@ PARALLEL_UPDATES = 1 class OhmeSelectDescription(OhmeEntityDescription, SelectEntityDescription): """Class to describe an Ohme select entity.""" - select_fn: Callable[[OhmeApiClient, Any], Awaitable[None]] + select_fn: Callable[[OhmeApiClient, Any], Coroutine[Any, Any, bool | None]] options: list[str] | None = None options_fn: Callable[[OhmeApiClient], list[str]] | None = None current_option_fn: Callable[[OhmeApiClient], str | None] diff --git a/homeassistant/components/ohme/sensor.py b/homeassistant/components/ohme/sensor.py index d0425040b53..daee8fff13e 100644 --- a/homeassistant/components/ohme/sensor.py +++ b/homeassistant/components/ohme/sensor.py @@ -34,7 +34,7 @@ PARALLEL_UPDATES = 0 class OhmeSensorDescription(OhmeEntityDescription, SensorEntityDescription): """Class describing Ohme sensor entities.""" - value_fn: Callable[[OhmeApiClient], str | int | float] + value_fn: Callable[[OhmeApiClient], str | int | float | None] SENSOR_CHARGE_SESSION = [ @@ -129,6 +129,6 @@ class OhmeSensor(OhmeEntity, SensorEntity): entity_description: OhmeSensorDescription @property - def native_value(self) -> str | int | float: + def native_value(self) -> str | int | float | None: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.client) diff --git a/homeassistant/components/ohme/services.py b/homeassistant/components/ohme/services.py index be044f01740..249fb1abdab 100644 --- a/homeassistant/components/ohme/services.py +++ b/homeassistant/components/ohme/services.py @@ -78,7 +78,7 @@ def async_setup_services(hass: HomeAssistant) -> None: """List of charge slots.""" client = __get_client(service_call) - return {"slots": client.slots} + return {"slots": [slot.to_dict() for slot in client.slots]} async def set_price_cap( service_call: ServiceCall, diff --git a/homeassistant/components/ohme/time.py b/homeassistant/components/ohme/time.py index 264b2afd41a..a0b1edb594a 100644 --- a/homeassistant/components/ohme/time.py +++ b/homeassistant/components/ohme/time.py @@ -1,8 +1,9 @@ """Platform for time.""" -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import time +from typing import Any from ohme import ApiException, OhmeApiClient @@ -22,7 +23,7 @@ PARALLEL_UPDATES = 1 class OhmeTimeDescription(OhmeEntityDescription, TimeEntityDescription): """Class describing Ohme time entities.""" - set_fn: Callable[[OhmeApiClient, time], Awaitable[None]] + set_fn: Callable[[OhmeApiClient, time], Coroutine[Any, Any, bool]] value_fn: Callable[[OhmeApiClient], time] diff --git a/requirements_all.txt b/requirements_all.txt index 6d9afb7c5e0..a12c6dc2444 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1559,7 +1559,7 @@ odp-amsterdam==6.0.2 oemthermostat==1.1.1 # homeassistant.components.ohme -ohme==1.4.1 +ohme==1.5.1 # homeassistant.components.ollama ollama==0.4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc90fec6d06..cd3728b3d88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1305,7 +1305,7 @@ objgraph==3.5.0 odp-amsterdam==6.0.2 # homeassistant.components.ohme -ohme==1.4.1 +ohme==1.5.1 # homeassistant.components.ollama ollama==0.4.7 diff --git a/tests/components/ohme/test_services.py b/tests/components/ohme/test_services.py index 2513635c1c2..c228ddcd9a7 100644 --- a/tests/components/ohme/test_services.py +++ b/tests/components/ohme/test_services.py @@ -1,7 +1,9 @@ """Tests for services.""" +from datetime import datetime from unittest.mock import AsyncMock, MagicMock +from ohme import ChargeSlot import pytest from syrupy.assertion import SnapshotAssertion @@ -30,11 +32,11 @@ async def test_list_charge_slots( await setup_integration(hass, mock_config_entry) mock_client.slots = [ - { - "start": "2024-12-30T04:00:00+00:00", - "end": "2024-12-30T04:30:39+00:00", - "energy": 2.042, - } + ChargeSlot( + datetime.fromisoformat("2024-12-30T04:00:00+00:00"), + datetime.fromisoformat("2024-12-30T04:30:39+00:00"), + 2.042, + ) ] assert snapshot == await hass.services.async_call( From f2ccd4626773ab9f349b884d5ef4280d87883b1e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 31 Mar 2025 12:20:06 +0200 Subject: [PATCH 104/258] Fix SmartThings being able to understand incomplete DRLC (#141907) --- .../components/smartthings/climate.py | 14 +- tests/components/smartthings/conftest.py | 1 + .../device_status/da_ac_rac_000003.json | 585 ++++++++++++++++++ .../fixtures/devices/da_ac_rac_000003.json | 217 +++++++ .../smartthings/snapshots/test_climate.ambr | 106 ++++ .../smartthings/snapshots/test_init.ambr | 33 + .../smartthings/snapshots/test_sensor.ambr | 429 +++++++++++++ 7 files changed, 1379 insertions(+), 6 deletions(-) create mode 100644 tests/components/smartthings/fixtures/device_status/da_ac_rac_000003.json create mode 100644 tests/components/smartthings/fixtures/devices/da_ac_rac_000003.json diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 9f94293d863..49499732c24 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -466,12 +466,14 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): Capability.DEMAND_RESPONSE_LOAD_CONTROL, Attribute.DEMAND_RESPONSE_LOAD_CONTROL_STATUS, ) - return { - "drlc_status_duration": drlc_status["duration"], - "drlc_status_level": drlc_status["drlcLevel"], - "drlc_status_start": drlc_status["start"], - "drlc_status_override": drlc_status["override"], - } + res = {} + for key in ("duration", "start", "override", "drlcLevel"): + if key in drlc_status: + dict_key = {"drlcLevel": "drlc_status_level"}.get( + key, f"drlc_status_{key}" + ) + res[dict_key] = drlc_status[key] + return res @property def fan_mode(self) -> str: diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index ef6b6f29011..5744adfef07 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -93,6 +93,7 @@ def mock_smartthings() -> Generator[AsyncMock]: params=[ "da_ac_airsensor_01001", "da_ac_rac_000001", + "da_ac_rac_000003", "da_ac_rac_100001", "da_ac_rac_01001", "multipurpose_sensor", diff --git a/tests/components/smartthings/fixtures/device_status/da_ac_rac_000003.json b/tests/components/smartthings/fixtures/device_status/da_ac_rac_000003.json new file mode 100644 index 00000000000..98434aa2c5a --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_ac_rac_000003.json @@ -0,0 +1,585 @@ +{ + "components": { + "main": { + "relativeHumidityMeasurement": { + "humidity": { + "value": 48, + "unit": "%", + "timestamp": "2025-03-27T05:12:16.158Z" + } + }, + "custom.airConditionerOdorController": { + "airConditionerOdorControllerProgress": { + "value": null + }, + "airConditionerOdorControllerState": { + "value": null + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 16, + "unit": "C", + "timestamp": "2025-03-13T09:29:37.008Z" + }, + "maximumSetpoint": { + "value": 30, + "unit": "C", + "timestamp": "2024-06-21T13:45:16.785Z" + } + }, + "airConditionerMode": { + "availableAcModes": { + "value": null + }, + "supportedAcModes": { + "value": ["cool", "dry", "wind", "auto"], + "timestamp": "2024-06-21T13:45:16.785Z" + }, + "airConditionerMode": { + "value": "cool", + "timestamp": "2025-03-13T09:29:36.789Z" + } + }, + "custom.spiMode": { + "spiMode": { + "value": "off", + "timestamp": "2025-02-08T08:54:15.661Z" + } + }, + "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": "ARTIK051_PRAC_20K", + "timestamp": "2025-03-27T05:12:15.284Z" + } + }, + "airQualitySensor": { + "airQuality": { + "value": null + } + }, + "custom.airConditionerOptionalMode": { + "supportedAcOptionalMode": { + "value": [ + "off", + "sleep", + "quiet", + "smart", + "speed", + "windFree", + "windFreeSleep" + ], + "timestamp": "2024-06-21T13:45:16.785Z" + }, + "acOptionalMode": { + "value": "off", + "timestamp": "2025-03-26T12:20:41.095Z" + } + }, + "switch": { + "switch": { + "value": "on", + "timestamp": "2025-03-27T05:41:42.291Z" + } + }, + "custom.airConditionerTropicalNightMode": { + "acTropicalNightModeLevel": { + "value": 0, + "timestamp": "2025-02-08T08:54:15.789Z" + } + }, + "ocf": { + "st": { + "value": null + }, + "mndt": { + "value": null + }, + "mnfv": { + "value": "ARTIK051_PRAC_20K_11230313", + "timestamp": "2024-06-21T13:58:04.085Z" + }, + "mnhw": { + "value": "ARTIK051", + "timestamp": "2024-06-21T13:51:35.294Z" + }, + "di": { + "value": "c76d6f38-1b7f-13dd-37b5-db18d5272783", + "timestamp": "2024-06-21T13:45:16.329Z" + }, + "mnsl": { + "value": "http://www.samsung.com", + "timestamp": "2024-06-21T13:51:35.980Z" + }, + "dmv": { + "value": "res.1.1.0,sh.1.1.0", + "timestamp": "2024-06-21T13:58:04.698Z" + }, + "n": { + "value": "Samsung Room A/C", + "timestamp": "2024-06-21T13:58:04.085Z" + }, + "mnmo": { + "value": "ARTIK051_PRAC_20K|10256941|60010534001411014600003200800000", + "timestamp": "2024-06-21T13:45:16.329Z" + }, + "vid": { + "value": "DA-AC-RAC-000003", + "timestamp": "2024-06-21T13:45:16.329Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2024-06-21T13:45:16.329Z" + }, + "mnml": { + "value": "http://www.samsung.com", + "timestamp": "2024-06-21T13:45:16.329Z" + }, + "mnpv": { + "value": "DAWIT 2.0", + "timestamp": "2024-06-21T13:51:35.294Z" + }, + "mnos": { + "value": "TizenRT 1.0 + IPv6", + "timestamp": "2024-06-21T13:51:35.294Z" + }, + "pi": { + "value": "c76d6f38-1b7f-13dd-37b5-db18d5272783", + "timestamp": "2024-06-21T13:45:16.329Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2024-06-21T13:45:16.329Z" + } + }, + "airConditionerFanMode": { + "fanMode": { + "value": "low", + "timestamp": "2025-03-26T12:20:41.393Z" + }, + "supportedAcFanModes": { + "value": ["auto", "low", "medium", "high", "turbo"], + "timestamp": "2024-06-21T13:45:16.785Z" + }, + "availableAcFanModes": { + "value": null + } + }, + "samsungce.dustFilterAlarm": { + "alarmThreshold": { + "value": 500, + "unit": "Hour", + "timestamp": "2025-02-08T08:54:15.473Z" + }, + "supportedAlarmThresholds": { + "value": [180, 300, 500, 700], + "unit": "Hour", + "timestamp": "2025-02-08T08:54:15.473Z" + } + }, + "custom.electricHepaFilter": { + "electricHepaFilterCapacity": { + "value": null + }, + "electricHepaFilterUsageStep": { + "value": null + }, + "electricHepaFilterLastResetDate": { + "value": null + }, + "electricHepaFilterStatus": { + "value": null + }, + "electricHepaFilterUsage": { + "value": null + }, + "electricHepaFilterResetType": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [ + "remoteControlStatus", + "airQualitySensor", + "dustSensor", + "odorSensor", + "veryFineDustSensor", + "custom.spiMode", + "custom.deodorFilter", + "custom.electricHepaFilter", + "custom.periodicSensing", + "custom.doNotDisturbMode", + "custom.airConditionerOdorController", + "samsungce.individualControlLock" + ], + "timestamp": "2025-02-08T08:54:15.355Z" + } + }, + "custom.ocfResourceVersion": { + "ocfResourceUpdatedTime": { + "value": null + }, + "ocfResourceVersion": { + "value": null + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 24040101, + "timestamp": "2024-06-21T13:45:16.348Z" + } + }, + "fanOscillationMode": { + "supportedFanOscillationModes": { + "value": ["fixed", "all", "vertical", "horizontal"], + "timestamp": "2025-02-08T08:54:15.797Z" + }, + "availableFanOscillationModes": { + "value": null + }, + "fanOscillationMode": { + "value": "fixed", + "timestamp": "2025-02-25T15:40:11.773Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 26, + "unit": "C", + "timestamp": "2025-03-26T14:19:08.047Z" + } + }, + "dustSensor": { + "dustLevel": { + "value": null + }, + "fineDustLevel": { + "value": null + } + }, + "custom.deviceReportStateConfiguration": { + "reportStateRealtimePeriod": { + "value": "disabled", + "timestamp": "2025-02-08T08:54:15.726Z" + }, + "reportStateRealtime": { + "value": { + "state": "enabled", + "duration": 10, + "unit": "minute" + }, + "timestamp": "2025-03-24T08:28:07.030Z" + }, + "reportStatePeriod": { + "value": "enabled", + "timestamp": "2025-02-08T08:54:15.726Z" + } + }, + "custom.periodicSensing": { + "automaticExecutionSetting": { + "value": null + }, + "automaticExecutionMode": { + "value": null + }, + "supportedAutomaticExecutionSetting": { + "value": null + }, + "supportedAutomaticExecutionMode": { + "value": null + }, + "periodicSensing": { + "value": null + }, + "periodicSensingInterval": { + "value": null + }, + "lastSensingTime": { + "value": null + }, + "lastSensingLevel": { + "value": null + }, + "periodicSensingStatus": { + "value": null + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": 24, + "unit": "C", + "timestamp": "2025-03-26T12:20:41.346Z" + } + }, + "demandResponseLoadControl": { + "drlcStatus": { + "value": { + "drlcType": 1, + "duration": 0, + "override": false + }, + "timestamp": "2025-03-24T04:56:36.855Z" + } + }, + "audioVolume": { + "volume": { + "value": 100, + "unit": "%", + "timestamp": "2025-02-08T08:54:15.789Z" + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "energy": 602171, + "deltaEnergy": 0, + "power": 0, + "powerEnergy": 0.0, + "persistedEnergy": 602171, + "energySaved": 0, + "persistedSavedEnergy": 0, + "start": "2025-03-27T05:29:22Z", + "end": "2025-03-27T05:40:02Z" + }, + "timestamp": "2025-03-27T05:40:02.686Z" + } + }, + "custom.autoCleaningMode": { + "supportedAutoCleaningModes": { + "value": null + }, + "timedCleanDuration": { + "value": null + }, + "operatingState": { + "value": null + }, + "timedCleanDurationRange": { + "value": null + }, + "supportedOperatingStates": { + "value": null + }, + "progress": { + "value": null + }, + "autoCleaningMode": { + "value": "off", + "timestamp": "2025-03-15T05:30:11.075Z" + } + }, + "samsungce.individualControlLock": { + "lockState": { + "value": null + } + }, + "refresh": {}, + "execute": { + "data": { + "value": null + } + }, + "samsungce.selfCheck": { + "result": { + "value": null + }, + "supportedActions": { + "value": ["start"], + "timestamp": "2024-06-21T13:45:16.348Z" + }, + "progress": { + "value": null + }, + "errors": { + "value": [], + "timestamp": "2025-02-08T08:54:15.048Z" + }, + "status": { + "value": null + } + }, + "custom.dustFilter": { + "dustFilterUsageStep": { + "value": 1, + "timestamp": "2025-02-08T08:54:15.473Z" + }, + "dustFilterUsage": { + "value": 69, + "timestamp": "2025-03-26T10:57:41.097Z" + }, + "dustFilterLastResetDate": { + "value": null + }, + "dustFilterStatus": { + "value": "normal", + "timestamp": "2025-02-08T08:54:15.473Z" + }, + "dustFilterCapacity": { + "value": 500, + "unit": "Hour", + "timestamp": "2025-02-08T08:54:15.473Z" + }, + "dustFilterResetType": { + "value": ["replaceable", "washable"], + "timestamp": "2025-02-08T08:54:15.473Z" + } + }, + "odorSensor": { + "odorLevel": { + "value": null + } + }, + "remoteControlStatus": { + "remoteControlEnabled": { + "value": null + } + }, + "custom.deodorFilter": { + "deodorFilterCapacity": { + "value": null + }, + "deodorFilterLastResetDate": { + "value": null + }, + "deodorFilterStatus": { + "value": null + }, + "deodorFilterResetType": { + "value": null + }, + "deodorFilterUsage": { + "value": null + }, + "deodorFilterUsageStep": { + "value": null + } + }, + "custom.energyType": { + "energyType": { + "value": "1.0", + "timestamp": "2024-06-21T13:45:16.785Z" + }, + "energySavingSupport": { + "value": true, + "timestamp": "2024-06-21T13:58:08.419Z" + }, + "drMaxDuration": { + "value": 99999999, + "unit": "min", + "timestamp": "2024-06-21T13:51:39.304Z" + }, + "energySavingLevel": { + "value": null + }, + "energySavingInfo": { + "value": null + }, + "supportedEnergySavingLevels": { + "value": null + }, + "energySavingOperation": { + "value": false, + "timestamp": "2025-02-08T08:54:16.767Z" + }, + "notificationTemplateID": { + "value": null + }, + "energySavingOperationSupport": { + "value": false, + "timestamp": "2025-03-24T04:56:36.855Z" + } + }, + "samsungce.softwareUpdate": { + "targetModule": { + "value": {}, + "timestamp": "2025-02-08T08:54:16.685Z" + }, + "otnDUID": { + "value": "MTCPH4AI4MTYO", + "timestamp": "2025-02-08T08:54:15.626Z" + }, + "lastUpdatedDate": { + "value": null + }, + "availableModules": { + "value": [], + "timestamp": "2025-02-08T08:54:15.626Z" + }, + "newVersionAvailable": { + "value": false, + "timestamp": "2025-02-08T08:54:15.626Z" + }, + "operatingState": { + "value": null + }, + "progress": { + "value": null + } + }, + "veryFineDustSensor": { + "veryFineDustLevel": { + "value": null + } + }, + "custom.veryFineDustFilter": { + "veryFineDustFilterStatus": { + "value": null + }, + "veryFineDustFilterResetType": { + "value": null + }, + "veryFineDustFilterUsage": { + "value": null + }, + "veryFineDustFilterLastResetDate": { + "value": null + }, + "veryFineDustFilterUsageStep": { + "value": null + }, + "veryFineDustFilterCapacity": { + "value": null + } + }, + "custom.doNotDisturbMode": { + "doNotDisturb": { + "value": null + }, + "startTime": { + "value": null + }, + "endTime": { + "value": null + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/da_ac_rac_000003.json b/tests/components/smartthings/fixtures/devices/da_ac_rac_000003.json new file mode 100644 index 00000000000..44dafc213f0 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_ac_rac_000003.json @@ -0,0 +1,217 @@ +{ + "items": [ + { + "deviceId": "c76d6f38-1b7f-13dd-37b5-db18d5272783", + "name": "Samsung Room A/C", + "label": "Office AirFree", + "manufacturerName": "Samsung Electronics", + "presentationId": "DA-AC-RAC-000003", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "403cd42e-f692-416c-91fd-1883c00e3262", + "ownerId": "dd474e5c-59c0-4bea-a319-ff5287fd3373", + "roomId": "dffe353e-b3c5-4a97-8a8a-797ccc649fab", + "deviceTypeName": "Samsung OCF Air Conditioner", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "ocf", + "version": 1 + }, + { + "id": "switch", + "version": 1 + }, + { + "id": "airConditionerMode", + "version": 1 + }, + { + "id": "airConditionerFanMode", + "version": 1 + }, + { + "id": "fanOscillationMode", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "relativeHumidityMeasurement", + "version": 1 + }, + { + "id": "airQualitySensor", + "version": 1 + }, + { + "id": "odorSensor", + "version": 1 + }, + { + "id": "dustSensor", + "version": 1 + }, + { + "id": "veryFineDustSensor", + "version": 1 + }, + { + "id": "audioVolume", + "version": 1 + }, + { + "id": "remoteControlStatus", + "version": 1 + }, + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "demandResponseLoadControl", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "custom.spiMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "custom.airConditionerOptionalMode", + "version": 1 + }, + { + "id": "custom.airConditionerTropicalNightMode", + "version": 1 + }, + { + "id": "custom.autoCleaningMode", + "version": 1 + }, + { + "id": "custom.deviceReportStateConfiguration", + "version": 1 + }, + { + "id": "custom.energyType", + "version": 1 + }, + { + "id": "custom.dustFilter", + "version": 1 + }, + { + "id": "custom.veryFineDustFilter", + "version": 1 + }, + { + "id": "custom.deodorFilter", + "version": 1 + }, + { + "id": "custom.electricHepaFilter", + "version": 1 + }, + { + "id": "custom.doNotDisturbMode", + "version": 1 + }, + { + "id": "custom.periodicSensing", + "version": 1 + }, + { + "id": "custom.airConditionerOdorController", + "version": 1 + }, + { + "id": "custom.ocfResourceVersion", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "samsungce.deviceIdentification", + "version": 1 + }, + { + "id": "samsungce.dustFilterAlarm", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.softwareUpdate", + "version": 1 + }, + { + "id": "samsungce.selfCheck", + "version": 1 + }, + { + "id": "samsungce.individualControlLock", + "version": 1 + } + ], + "categories": [ + { + "name": "AirConditioner", + "categoryType": "manufacturer" + } + ] + } + ], + "createTime": "2024-06-21T13:45:16.238Z", + "profile": { + "id": "cedae6e3-1ec9-37e3-9aba-f717518156b8" + }, + "ocf": { + "ocfDeviceType": "oic.d.airconditioner", + "name": "Samsung Room A/C", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "1.2.1", + "manufacturerName": "Samsung Electronics", + "modelNumber": "ARTIK051_PRAC_20K|10256941|60010534001411014600003200800000", + "platformVersion": "DAWIT 2.0", + "platformOS": "TizenRT 1.0 + IPv6", + "hwVersion": "ARTIK051", + "firmwareVersion": "ARTIK051_PRAC_20K_11230313", + "vendorId": "DA-AC-RAC-000003", + "vendorResourceClientServerVersion": "ARTIK051 Release 2.211222.1", + "lastSignupTime": "2024-06-21T13:45:08.592221Z", + "transferCandidate": false, + "additionalAuthCodeRequired": false + }, + "type": "OCF", + "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 17e25421fec..19cfe971d7f 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -228,6 +228,112 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ac_rac_000003][climate.office_airfree-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'auto', + 'low', + 'medium', + 'high', + 'turbo', + ]), + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'preset_modes': list([ + 'windFree', + ]), + 'swing_modes': list([ + 'off', + 'both', + 'vertical', + 'horizontal', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.office_airfree', + 'has_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': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ac_rac_000003][climate.office_airfree-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 26, + 'drlc_status_duration': 0, + 'drlc_status_override': False, + 'fan_mode': 'low', + 'fan_modes': list([ + 'auto', + 'low', + 'medium', + 'high', + 'turbo', + ]), + 'friendly_name': 'Office AirFree', + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'preset_mode': None, + 'preset_modes': list([ + 'windFree', + ]), + 'supported_features': , + 'swing_mode': 'off', + 'swing_modes': list([ + 'off', + 'both', + 'vertical', + 'horizontal', + ]), + 'temperature': 24, + }), + 'context': , + 'entity_id': 'climate.office_airfree', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- # name: test_all_entities[da_ac_rac_01001][climate.aire_dormitorio_principal-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 6a402182b82..052d15bd1ae 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -365,6 +365,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_ac_rac_000003] + 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': 'ARTIK051', + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + 'c76d6f38-1b7f-13dd-37b5-db18d5272783', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'ARTIK051_PRAC_20K', + 'model_id': None, + 'name': 'Office AirFree', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'ARTIK051_PRAC_20K_11230313', + 'via_device_id': None, + }) +# --- # name: test_devices[da_ac_rac_01001] DeviceRegistryEntrySnapshot({ 'area_id': 'theater', diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 416a3d15947..73bbc96bc85 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -1513,6 +1513,435 @@ 'state': '100', }) # --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_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.office_airfree_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': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_energy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Office AirFree Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.office_airfree_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '602.171', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_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.office_airfree_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': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_energy_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Office AirFree Energy difference', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.office_airfree_energy_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_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.office_airfree_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': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_energySaved_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_energy_saved-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Office AirFree Energy saved', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.office_airfree_energy_saved', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_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.office_airfree_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': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_relativeHumidityMeasurement_humidity_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Office AirFree Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.office_airfree_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '48', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_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.office_airfree_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': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_power_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Office AirFree Power', + 'power_consumption_end': '2025-03-27T05:40:02Z', + 'power_consumption_start': '2025-03-27T05:29:22Z', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.office_airfree_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_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.office_airfree_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': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_power_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Office AirFree Power energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.office_airfree_power_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_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.office_airfree_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': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Office AirFree Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.office_airfree_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_volume-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.office_airfree_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': 'Volume', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'audio_volume', + 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_audioVolume_volume_volume', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[da_ac_rac_000003][sensor.office_airfree_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Office AirFree Volume', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.office_airfree_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- # name: test_all_entities[da_ac_rac_01001][sensor.aire_dormitorio_principal_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 216b8ef400b3cf19176477b4cad6aa509ece204f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 31 Mar 2025 12:36:46 +0200 Subject: [PATCH 105/258] Don't create SmartThings entities for disabled components (#141909) --- .../components/smartthings/__init__.py | 29 +- tests/components/smartthings/conftest.py | 1 + .../device_status/da_ref_normal_01011.json | 933 ++++++++++++++++++ .../fixtures/devices/da_ref_normal_01011.json | 521 ++++++++++ .../snapshots/test_binary_sensor.ambr | 144 +++ .../smartthings/snapshots/test_init.ambr | 33 + .../smartthings/snapshots/test_sensor.ambr | 277 ++++++ 7 files changed, 1933 insertions(+), 5 deletions(-) create mode 100644 tests/components/smartthings/fixtures/device_status/da_ref_normal_01011.json create mode 100644 tests/components/smartthings/fixtures/devices/da_ref_normal_01011.json diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 346d5e66b42..c8ca1a819e0 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -478,7 +478,27 @@ def process_status(status: dict[str, ComponentStatus]) -> dict[str, ComponentSta if (main_component := status.get(MAIN)) is None: return status if ( - disabled_capabilities_capability := main_component.get( + disabled_components_capability := main_component.get( + Capability.CUSTOM_DISABLED_COMPONENTS + ) + ) is not None: + disabled_components = cast( + list[str], + disabled_components_capability[Attribute.DISABLED_COMPONENTS].value, + ) + if disabled_components is not None: + for component in disabled_components: + if component in status: + del status[component] + for component_status in status.values(): + process_component_status(component_status) + return status + + +def process_component_status(status: ComponentStatus) -> None: + """Remove disabled capabilities from component status.""" + if ( + disabled_capabilities_capability := status.get( Capability.CUSTOM_DISABLED_CAPABILITIES ) ) is not None: @@ -488,9 +508,8 @@ def process_status(status: dict[str, ComponentStatus]) -> dict[str, ComponentSta ) if disabled_capabilities is not None: for capability in disabled_capabilities: - if capability in main_component and ( + if capability in status and ( capability not in KEEP_CAPABILITY_QUIRK - or not KEEP_CAPABILITY_QUIRK[capability](main_component[capability]) + or not KEEP_CAPABILITY_QUIRK[capability](status[capability]) ): - del main_component[capability] - return status + del status[capability] diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 5744adfef07..277c327744f 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -106,6 +106,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "ge_in_wall_smart_dimmer", "centralite", "da_ref_normal_000001", + "da_ref_normal_01011", "vd_network_audio_002s", "vd_sensor_light_2023", "iphone", diff --git a/tests/components/smartthings/fixtures/device_status/da_ref_normal_01011.json b/tests/components/smartthings/fixtures/device_status/da_ref_normal_01011.json new file mode 100644 index 00000000000..350a0ee14bb --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_ref_normal_01011.json @@ -0,0 +1,933 @@ +{ + "components": { + "pantry-01": { + "samsungce.foodDefrost": { + "supportedOptions": { + "value": null + }, + "foodType": { + "value": null + }, + "weight": { + "value": null + }, + "operationTime": { + "value": null + }, + "remainingTime": { + "value": null + } + }, + "samsungce.fridgePantryInfo": { + "name": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "samsungce.meatAging": { + "zoneInfo": { + "value": null + }, + "supportedMeatTypes": { + "value": null + }, + "supportedAgingMethods": { + "value": null + }, + "status": { + "value": null + } + }, + "samsungce.fridgePantryMode": { + "mode": { + "value": null + }, + "supportedModes": { + "value": null + } + } + }, + "pantry-02": { + "samsungce.foodDefrost": { + "supportedOptions": { + "value": null + }, + "foodType": { + "value": null + }, + "weight": { + "value": null + }, + "operationTime": { + "value": null + }, + "remainingTime": { + "value": null + } + }, + "samsungce.fridgePantryInfo": { + "name": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "samsungce.meatAging": { + "zoneInfo": { + "value": null + }, + "supportedMeatTypes": { + "value": null + }, + "supportedAgingMethods": { + "value": null + }, + "status": { + "value": null + } + }, + "samsungce.fridgePantryMode": { + "mode": { + "value": null + }, + "supportedModes": { + "value": null + } + } + }, + "icemaker": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "switch": { + "switch": { + "value": null + } + } + }, + "onedoor": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "contactSensor": { + "contact": { + "value": null + } + }, + "samsungce.unavailableCapabilities": { + "unavailableCommands": { + "value": [], + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.freezerConvertMode", "custom.fridgeMode"], + "timestamp": "2024-12-01T18:22:20.155Z" + } + }, + "samsungce.temperatureSetting": { + "supportedDesiredTemperatures": { + "value": null + }, + "desiredTemperature": { + "value": null + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": null + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": null + }, + "maximumSetpoint": { + "value": null + } + }, + "samsungce.freezerConvertMode": { + "supportedFreezerConvertModes": { + "value": null + }, + "freezerConvertMode": { + "value": null + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": null + } + } + }, + "scale-10": { + "samsungce.connectionState": { + "connectionState": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "samsungce.weightMeasurement": { + "weight": { + "value": null + } + }, + "samsungce.weightMeasurementCalibration": {} + }, + "scale-11": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "samsungce.weightMeasurement": { + "weight": { + "value": null + } + } + }, + "cooler": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2025-03-30T18:36:45.151Z" + } + }, + "samsungce.unavailableCapabilities": { + "unavailableCommands": { + "value": [], + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["custom.fridgeMode", "samsungce.temperatureSetting"], + "timestamp": "2024-12-01T18:22:22.081Z" + } + }, + "samsungce.temperatureSetting": { + "supportedDesiredTemperatures": { + "value": null + }, + "desiredTemperature": { + "value": null + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 6, + "unit": "C", + "timestamp": "2025-03-30T17:41:42.863Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 1, + "unit": "C", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "maximumSetpoint": { + "value": 7, + "unit": "C", + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": { + "minimum": 1, + "maximum": 7, + "step": 1 + }, + "unit": "C", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "coolingSetpoint": { + "value": 6, + "unit": "C", + "timestamp": "2025-03-30T17:33:48.530Z" + } + } + }, + "freezer": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2024-12-01T18:22:19.331Z" + } + }, + "samsungce.unavailableCapabilities": { + "unavailableCommands": { + "value": [], + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [ + "custom.fridgeMode", + "samsungce.temperatureSetting", + "samsungce.freezerConvertMode" + ], + "timestamp": "2024-12-01T18:22:22.081Z" + } + }, + "samsungce.temperatureSetting": { + "supportedDesiredTemperatures": { + "value": null + }, + "desiredTemperature": { + "value": null + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": -17, + "unit": "C", + "timestamp": "2025-03-30T17:35:48.599Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": -23, + "unit": "C", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "maximumSetpoint": { + "value": -15, + "unit": "C", + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "samsungce.freezerConvertMode": { + "supportedFreezerConvertModes": { + "value": null + }, + "freezerConvertMode": { + "value": null + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": { + "minimum": -23, + "maximum": -15, + "step": 1 + }, + "unit": "C", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "coolingSetpoint": { + "value": -17, + "unit": "C", + "timestamp": "2025-03-30T17:32:34.710Z" + } + } + }, + "main": { + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2025-03-30T18:36:45.151Z" + } + }, + "samsungce.fridgeWelcomeLighting": { + "detectionProximity": { + "value": null + }, + "supportedDetectionProximities": { + "value": null + }, + "status": { + "value": null + } + }, + "samsungce.viewInside": { + "supportedFocusAreas": { + "value": null + }, + "contents": { + "value": null + }, + "lastUpdatedTime": { + "value": null + } + }, + "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_REF_21K", + "timestamp": "2025-03-23T21:53:15.900Z" + } + }, + "samsungce.quickControl": { + "version": { + "value": "1.0", + "timestamp": "2025-02-12T21:52:01.494Z" + } + }, + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "ocf": { + "st": { + "value": null + }, + "mndt": { + "value": null + }, + "mnfv": { + "value": "A-RFWW-TP1-22-REV1_20241030", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "mnhw": { + "value": "Realtek", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "di": { + "value": "5758b2ec-563e-f39b-ec39-208e54aabf60", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "mnsl": { + "value": "http://www.samsung.com", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "dmv": { + "value": "1.2.1", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "n": { + "value": "Samsung-Refrigerator", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "mnmo": { + "value": "TP1X_REF_21K|00156941|00050126001611304100000030010000", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "vid": { + "value": "DA-REF-NORMAL-01011", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "mnml": { + "value": "http://www.samsung.com", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "mnpv": { + "value": "DAWIT 2.0", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "mnos": { + "value": "TizenRT 3.1", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "pi": { + "value": "5758b2ec-563e-f39b-ec39-208e54aabf60", + "timestamp": "2025-02-12T21:51:58.927Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2025-02-12T21:51:58.927Z" + } + }, + "samsungce.fridgeVacationMode": { + "vacationMode": { + "value": "off", + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [ + "temperatureMeasurement", + "thermostatCoolingSetpoint", + "custom.fridgeMode", + "custom.deodorFilter", + "custom.waterFilter", + "custom.dustFilter", + "samsungce.viewInside", + "samsungce.fridgeWelcomeLighting", + "samsungce.sabbathMode" + ], + "timestamp": "2025-02-12T21:52:01.494Z" + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 24090102, + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "sec.diagnosticsInformation": { + "logType": { + "value": ["errCode", "dump"], + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "endpoint": { + "value": "SSM", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "minVersion": { + "value": "3.0", + "timestamp": "2025-02-12T21:52:00.460Z" + }, + "signinPermission": { + "value": null + }, + "setupId": { + "value": "RB0", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "protocolType": { + "value": "ble_ocf", + "timestamp": "2025-02-12T21:52:00.460Z" + }, + "tsId": { + "value": "DA01", + "timestamp": "2025-02-12T21:52:00.460Z" + }, + "mnId": { + "value": "0AJT", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "dumpType": { + "value": "file", + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": null + } + }, + "custom.deviceReportStateConfiguration": { + "reportStateRealtimePeriod": { + "value": null + }, + "reportStateRealtime": { + "value": { + "state": "disabled" + }, + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "reportStatePeriod": { + "value": "enabled", + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": null + } + }, + "custom.disabledComponents": { + "disabledComponents": { + "value": [ + "icemaker", + "icemaker-02", + "icemaker-03", + "pantry-01", + "pantry-02", + "scale-10", + "scale-11", + "cvroom", + "onedoor" + ], + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "demandResponseLoadControl": { + "drlcStatus": { + "value": { + "drlcType": 1, + "drlcLevel": 0, + "duration": 0, + "override": false + }, + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "samsungce.sabbathMode": { + "supportedActions": { + "value": null + }, + "status": { + "value": null + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "energy": 66571, + "deltaEnergy": 19, + "power": 61, + "powerEnergy": 18.91178222020467, + "persistedEnergy": 0, + "energySaved": 0, + "persistedSavedEnergy": 0, + "start": "2025-03-30T18:21:37Z", + "end": "2025-03-30T18:38:18Z" + }, + "timestamp": "2025-03-30T18:38:18.219Z" + } + }, + "refresh": {}, + "execute": { + "data": { + "value": null + } + }, + "sec.wifiConfiguration": { + "autoReconnection": { + "value": true, + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "minVersion": { + "value": "1.0", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "supportedWiFiFreq": { + "value": ["2.4G"], + "timestamp": "2024-12-01T18:22:19.331Z" + }, + "supportedAuthType": { + "value": ["OPEN", "WEP", "WPA-PSK", "WPA2-PSK", "SAE"], + "timestamp": "2024-12-01T18:22:19.331Z" + }, + "protocolType": { + "value": ["helper_hotspot"], + "timestamp": "2024-12-01T18:22:19.331Z" + } + }, + "samsungce.selfCheck": { + "result": { + "value": "passed", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "supportedActions": { + "value": ["start"], + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "progress": { + "value": null + }, + "errors": { + "value": [], + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "status": { + "value": "ready", + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "custom.dustFilter": { + "dustFilterUsageStep": { + "value": null + }, + "dustFilterUsage": { + "value": null + }, + "dustFilterLastResetDate": { + "value": null + }, + "dustFilterStatus": { + "value": null + }, + "dustFilterCapacity": { + "value": null + }, + "dustFilterResetType": { + "value": null + } + }, + "refrigeration": { + "defrost": { + "value": null + }, + "rapidCooling": { + "value": "off", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "rapidFreezing": { + "value": "off", + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "custom.deodorFilter": { + "deodorFilterCapacity": { + "value": null + }, + "deodorFilterLastResetDate": { + "value": null + }, + "deodorFilterStatus": { + "value": null + }, + "deodorFilterResetType": { + "value": null + }, + "deodorFilterUsage": { + "value": null + }, + "deodorFilterUsageStep": { + "value": null + } + }, + "samsungce.powerCool": { + "activated": { + "value": false, + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "custom.energyType": { + "energyType": { + "value": "2.0", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "energySavingSupport": { + "value": true, + "timestamp": "2025-03-06T23:10:37.429Z" + }, + "drMaxDuration": { + "value": 99999999, + "unit": "min", + "timestamp": "2024-12-01T18:22:20.756Z" + }, + "energySavingLevel": { + "value": 1, + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "energySavingInfo": { + "value": null + }, + "supportedEnergySavingLevels": { + "value": [1, 2], + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "energySavingOperation": { + "value": false, + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "notificationTemplateID": { + "value": null + }, + "energySavingOperationSupport": { + "value": true, + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "samsungce.softwareUpdate": { + "targetModule": { + "value": {}, + "timestamp": "2024-12-01T18:55:10.062Z" + }, + "otnDUID": { + "value": "MTCB2ZD4B6BT4", + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "lastUpdatedDate": { + "value": null + }, + "availableModules": { + "value": [], + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "newVersionAvailable": { + "value": false, + "timestamp": "2024-12-01T18:22:19.337Z" + }, + "operatingState": { + "value": "none", + "timestamp": "2024-12-01T18:28:40.492Z" + }, + "progress": { + "value": 0, + "unit": "%", + "timestamp": "2024-12-01T18:43:42.645Z" + } + }, + "samsungce.powerFreeze": { + "activated": { + "value": false, + "timestamp": "2024-12-01T18:22:19.337Z" + } + }, + "custom.waterFilter": { + "waterFilterUsageStep": { + "value": null + }, + "waterFilterResetType": { + "value": null + }, + "waterFilterCapacity": { + "value": null + }, + "waterFilterLastResetDate": { + "value": null + }, + "waterFilterUsage": { + "value": null + }, + "waterFilterStatus": { + "value": null + } + } + }, + "cvroom": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "contactSensor": { + "contact": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": null + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": null + } + } + }, + "icemaker-02": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "switch": { + "switch": { + "value": null + } + } + }, + "icemaker-03": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "switch": { + "switch": { + "value": null + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/da_ref_normal_01011.json b/tests/components/smartthings/fixtures/devices/da_ref_normal_01011.json new file mode 100644 index 00000000000..9be5db0bda9 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_ref_normal_01011.json @@ -0,0 +1,521 @@ +{ + "items": [ + { + "deviceId": "5758b2ec-563e-f39b-ec39-208e54aabf60", + "name": "Samsung-Refrigerator", + "label": "Frigo", + "manufacturerName": "Samsung Electronics", + "presentationId": "DA-REF-NORMAL-01011", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "d91ee683-be36-4124-9200-c0030253fbc2", + "ownerId": "60b5179d-607f-f754-a648-6e1e21aeeb31", + "roomId": "c4f98377-534d-422f-b061-a4f3e281ddf5", + "deviceTypeName": "Samsung OCF Refrigerator", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "ocf", + "version": 1 + }, + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "demandResponseLoadControl", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "refrigeration", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.deodorFilter", + "version": 1 + }, + { + "id": "custom.dustFilter", + "version": 1 + }, + { + "id": "custom.deviceReportStateConfiguration", + "version": 1 + }, + { + "id": "custom.energyType", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.disabledComponents", + "version": 1 + }, + { + "id": "custom.waterFilter", + "version": 1 + }, + { + "id": "samsungce.softwareUpdate", + "version": 1 + }, + { + "id": "samsungce.deviceIdentification", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.fridgeVacationMode", + "version": 1 + }, + { + "id": "samsungce.powerCool", + "version": 1 + }, + { + "id": "samsungce.powerFreeze", + "version": 1 + }, + { + "id": "samsungce.sabbathMode", + "version": 1 + }, + { + "id": "samsungce.selfCheck", + "version": 1 + }, + { + "id": "samsungce.viewInside", + "version": 1 + }, + { + "id": "samsungce.fridgeWelcomeLighting", + "version": 1 + }, + { + "id": "samsungce.quickControl", + "version": 1 + }, + { + "id": "sec.diagnosticsInformation", + "version": 1 + }, + { + "id": "sec.wifiConfiguration", + "version": 1 + } + ], + "categories": [ + { + "name": "Refrigerator", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "freezer", + "label": "freezer", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.temperatureSetting", + "version": 1 + }, + { + "id": "samsungce.freezerConvertMode", + "version": 1 + }, + { + "id": "samsungce.unavailableCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "cooler", + "label": "cooler", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.temperatureSetting", + "version": 1 + }, + { + "id": "samsungce.unavailableCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "cvroom", + "label": "cvroom", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "onedoor", + "label": "onedoor", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.temperatureSetting", + "version": 1 + }, + { + "id": "samsungce.freezerConvertMode", + "version": 1 + }, + { + "id": "samsungce.unavailableCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "icemaker", + "label": "icemaker", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "icemaker-02", + "label": "icemaker-02", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "icemaker-03", + "label": "icemaker-03", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "scale-10", + "label": "scale-10", + "capabilities": [ + { + "id": "samsungce.weightMeasurement", + "version": 1 + }, + { + "id": "samsungce.weightMeasurementCalibration", + "version": 1 + }, + { + "id": "samsungce.connectionState", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "scale-11", + "label": "scale-11", + "capabilities": [ + { + "id": "samsungce.weightMeasurement", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "pantry-01", + "label": "pantry-01", + "capabilities": [ + { + "id": "samsungce.fridgePantryInfo", + "version": 1 + }, + { + "id": "samsungce.fridgePantryMode", + "version": 1 + }, + { + "id": "samsungce.meatAging", + "version": 1 + }, + { + "id": "samsungce.foodDefrost", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "pantry-02", + "label": "pantry-02", + "capabilities": [ + { + "id": "samsungce.fridgePantryInfo", + "version": 1 + }, + { + "id": "samsungce.fridgePantryMode", + "version": 1 + }, + { + "id": "samsungce.meatAging", + "version": 1 + }, + { + "id": "samsungce.foodDefrost", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + } + ], + "createTime": "2024-12-01T18:22:14.880Z", + "profile": { + "id": "37c7b355-bdaa-371b-b246-dbdf2a7f9c84" + }, + "ocf": { + "ocfDeviceType": "oic.d.refrigerator", + "name": "Samsung-Refrigerator", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "1.2.1", + "manufacturerName": "Samsung Electronics", + "modelNumber": "TP1X_REF_21K|00156941|00050126001611304100000030010000", + "platformVersion": "DAWIT 2.0", + "platformOS": "TizenRT 3.1", + "hwVersion": "Realtek", + "firmwareVersion": "A-RFWW-TP1-22-REV1_20241030", + "vendorId": "DA-REF-NORMAL-01011", + "vendorResourceClientServerVersion": "Realtek Release 3.1.240221", + "lastSignupTime": "2024-12-01T18:22:14.807976528Z", + "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 d6a5ac6a4e7..d41c36aea64 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -809,6 +809,150 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_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.frigo_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': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_contactSensor_contact_contact', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_cooler_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Frigo Cooler door', + }), + 'context': , + 'entity_id': 'binary_sensor.frigo_cooler_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_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_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_contactSensor_contact_contact', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Frigo Door', + }), + 'context': , + 'entity_id': 'binary_sensor.frigo_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_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.frigo_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': 'Freezer door', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'freezer_door', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_freezer_contactSensor_contact_contact', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_freezer_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Frigo Freezer door', + }), + 'context': , + 'entity_id': 'binary_sensor.frigo_freezer_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_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 052d15bd1ae..8ec97af7d84 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -629,6 +629,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_ref_normal_01011] + 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', + '5758b2ec-563e-f39b-ec39-208e54aabf60', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'TP1X_REF_21K', + 'model_id': None, + 'name': 'Frigo', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'A-RFWW-TP1-22-REV1_20241030', + 'via_device_id': None, + }) +# --- # name: test_devices[da_rvc_normal_000001] DeviceRegistryEntrySnapshot({ 'area_id': 'theater', diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 73bbc96bc85..7be10ebac91 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -4049,6 +4049,283 @@ 'state': '0.0135559777781698', }) # --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_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.frigo_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': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_energy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Frigo Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frigo_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '66.571', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_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.frigo_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': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Frigo Energy difference', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frigo_energy_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.019', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_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.frigo_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': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_energySaved_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy_saved-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Frigo Energy saved', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frigo_energy_saved', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_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.frigo_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': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_power_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Frigo Power', + 'power_consumption_end': '2025-03-30T18:38:18Z', + 'power_consumption_start': '2025-03-30T18:21:37Z', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frigo_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '61', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_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.frigo_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': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_power_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Frigo Power energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frigo_power_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0189117822202047', + }) +# --- # name: test_all_entities[da_rvc_normal_000001][sensor.robot_vacuum_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From b014219fdd66230aa05faedf0426a82fc55fd84f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:18:44 +0200 Subject: [PATCH 106/258] Correct further sensor categorizations in AVM Fritz!Box tools (#141911) mark margin and attenuation as diagnostic and disable them by default --- homeassistant/components/fritz/sensor.py | 8 ++++++++ tests/components/fritz/snapshots/test_sensor.ambr | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 88de9ebdefc..243b3b5eb4c 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -238,6 +238,8 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( key="link_noise_margin_sent", translation_key="link_noise_margin_sent", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, value_fn=_retrieve_link_noise_margin_sent_state, is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION, ), @@ -245,6 +247,8 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( key="link_noise_margin_received", translation_key="link_noise_margin_received", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, value_fn=_retrieve_link_noise_margin_received_state, is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION, ), @@ -252,6 +256,8 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( key="link_attenuation_sent", translation_key="link_attenuation_sent", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, value_fn=_retrieve_link_attenuation_sent_state, is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION, ), @@ -259,6 +265,8 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( key="link_attenuation_received", translation_key="link_attenuation_received", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, value_fn=_retrieve_link_attenuation_received_state, is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION, ), diff --git a/tests/components/fritz/snapshots/test_sensor.ambr b/tests/components/fritz/snapshots/test_sensor.ambr index ffede386099..ffdd3d23f50 100644 --- a/tests/components/fritz/snapshots/test_sensor.ambr +++ b/tests/components/fritz/snapshots/test_sensor.ambr @@ -357,7 +357,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.mock_title_link_download_noise_margin', 'has_entity_name': True, 'hidden_by': None, @@ -405,7 +405,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.mock_title_link_download_power_attenuation', 'has_entity_name': True, 'hidden_by': None, @@ -502,7 +502,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.mock_title_link_upload_noise_margin', 'has_entity_name': True, 'hidden_by': None, @@ -550,7 +550,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.mock_title_link_upload_power_attenuation', 'has_entity_name': True, 'hidden_by': None, From c0c2edb90a7e3be5e5f03d4934e14834c44537c4 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Mon, 31 Mar 2025 15:29:17 +0200 Subject: [PATCH 107/258] Add None check to azure_storage (#141922) --- homeassistant/components/azure_storage/backup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/azure_storage/backup.py b/homeassistant/components/azure_storage/backup.py index 4d897126d3d..4a9254213dc 100644 --- a/homeassistant/components/azure_storage/backup.py +++ b/homeassistant/components/azure_storage/backup.py @@ -175,7 +175,8 @@ class AzureStorageBackupAgent(BackupAgent): """Find a blob by backup id.""" async for blob in self._client.list_blobs(include="metadata"): if ( - backup_id == blob.metadata.get("backup_id", "") + blob.metadata is not None + and backup_id == blob.metadata.get("backup_id", "") and blob.metadata.get("metadata_version") == METADATA_VERSION ): return blob From 6d022ff4e053688e8540e20154a997582025160a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 31 Mar 2025 18:33:45 +0200 Subject: [PATCH 108/258] Revert PR 136314 (Cleanup map references in lovelace) (#141928) * Revert PR 136314 (Cleanup map references in lovelace) * Update homeassistant/components/lovelace/__init__.py Co-authored-by: Martin Hjelmare * Fix dashboard creation * Update homeassistant/components/lovelace/__init__.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/lovelace/__init__.py | 32 +++++++++- tests/components/lovelace/test_init.py | 58 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 4d8472da9a2..c0262f42f6c 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -6,7 +6,7 @@ from typing import Any import voluptuous as vol -from homeassistant.components import frontend, websocket_api +from homeassistant.components import frontend, onboarding, websocket_api from homeassistant.config import ( async_hass_config_yaml, async_process_component_and_handle_errors, @@ -17,6 +17,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.frame import report_usage from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from homeassistant.util import slugify @@ -282,6 +283,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: STORAGE_DASHBOARD_UPDATE_FIELDS, ).async_setup(hass) + def create_map_dashboard() -> None: + """Create a map dashboard.""" + hass.async_create_task(_create_map_dashboard(hass, dashboards_collection)) + + if not onboarding.async_is_onboarded(hass): + onboarding.async_add_listener(hass, create_map_dashboard) + return True @@ -323,3 +331,25 @@ def _register_panel( kwargs["sidebar_icon"] = config.get(CONF_ICON, DEFAULT_ICON) frontend.async_register_built_in_panel(hass, DOMAIN, **kwargs) + + +async def _create_map_dashboard( + hass: HomeAssistant, dashboards_collection: dashboard.DashboardsCollection +) -> None: + """Create a map dashboard.""" + translations = await async_get_translations( + hass, hass.config.language, "dashboard", {onboarding.DOMAIN} + ) + title = translations["component.onboarding.dashboard.map.title"] + + await dashboards_collection.async_create_item( + { + CONF_ALLOW_SINGLE_WORD: True, + CONF_ICON: "mdi:map", + CONF_TITLE: title, + CONF_URL_PATH: "map", + } + ) + + map_store = hass.data[LOVELACE_DATA].dashboards["map"] + await map_store.async_save({"strategy": {"type": "map"}}) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index f35f7369f93..4c7cc96504b 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -13,6 +13,16 @@ from homeassistant.setup import async_setup_component from tests.typing import WebSocketGenerator +@pytest.fixture +def mock_onboarding_not_done() -> Generator[MagicMock]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + yield mock_onboarding + + @pytest.fixture def mock_onboarding_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" @@ -23,6 +33,15 @@ def mock_onboarding_done() -> Generator[MagicMock]: yield mock_onboarding +@pytest.fixture +def mock_add_onboarding_listener() -> Generator[MagicMock]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_add_listener", + ) as mock_add_onboarding_listener: + yield mock_add_onboarding_listener + + async def test_create_dashboards_when_onboarded( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -41,6 +60,45 @@ async def test_create_dashboards_when_onboarded( assert response["result"] == [] +async def test_create_dashboards_when_not_onboarded( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + mock_add_onboarding_listener, + mock_onboarding_not_done, +) -> None: + """Test we automatically create dashboards when not onboarded.""" + client = await hass_ws_client(hass) + + assert await async_setup_component(hass, "lovelace", {}) + + # Call onboarding listener + mock_add_onboarding_listener.mock_calls[0][1][1]() + await hass.async_block_till_done() + + # List dashboards + await client.send_json_auto_id({"type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "icon": "mdi:map", + "id": "map", + "mode": "storage", + "require_admin": False, + "show_in_sidebar": True, + "title": "Map", + "url_path": "map", + } + ] + + # List map dashboard config + await client.send_json_auto_id({"type": "lovelace/config", "url_path": "map"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"strategy": {"type": "map"}} + + @pytest.mark.parametrize("integration_frame_path", ["custom_components/my_integration"]) @pytest.mark.usefixtures("mock_integration_frame") async def test_hass_data_compatibility( From 8f9572bb05a6b3a6738315ea2044fdb3e09932ad Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 31 Mar 2025 13:29:07 -0500 Subject: [PATCH 109/258] Add preannounce boolean for announce/start conversation (#141930) * Add preannounce boolean * Fix disabling preannounce in wizard * Fix casing * Fix type of preannounce_media_id * Adjust description of preannounce_media_id --- .../components/assist_satellite/__init__.py | 6 ++++-- .../components/assist_satellite/entity.py | 20 ++++++++++++------- .../components/assist_satellite/services.yaml | 10 ++++++++++ .../components/assist_satellite/strings.json | 16 +++++++++++---- .../assist_satellite/websocket_api.py | 2 +- .../assist_satellite/test_entity.py | 12 +++++------ .../esphome/test_assist_satellite.py | 8 ++++---- 7 files changed, 50 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/assist_satellite/__init__.py b/homeassistant/components/assist_satellite/__init__.py index bc2157b10b2..3338f223bc9 100644 --- a/homeassistant/components/assist_satellite/__init__.py +++ b/homeassistant/components/assist_satellite/__init__.py @@ -60,7 +60,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: { vol.Optional("message"): str, vol.Optional("media_id"): str, - vol.Optional("preannounce_media_id"): vol.Any(str, None), + vol.Optional("preannounce"): bool, + vol.Optional("preannounce_media_id"): str, } ), cv.has_at_least_one_key("message", "media_id"), @@ -75,7 +76,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: { vol.Optional("start_message"): str, vol.Optional("start_media_id"): str, - vol.Optional("preannounce_media_id"): vol.Any(str, None), + vol.Optional("preannounce"): bool, + vol.Optional("preannounce_media_id"): str, vol.Optional("extra_system_prompt"): str, } ), diff --git a/homeassistant/components/assist_satellite/entity.py b/homeassistant/components/assist_satellite/entity.py index 7b4c1b92d8c..dc20c7650d7 100644 --- a/homeassistant/components/assist_satellite/entity.py +++ b/homeassistant/components/assist_satellite/entity.py @@ -180,7 +180,8 @@ class AssistSatelliteEntity(entity.Entity): self, message: str | None = None, media_id: str | None = None, - preannounce_media_id: str | None = PREANNOUNCE_URL, + preannounce: bool = True, + preannounce_media_id: str = PREANNOUNCE_URL, ) -> None: """Play and show an announcement on the satellite. @@ -190,8 +191,8 @@ class AssistSatelliteEntity(entity.Entity): If media_id is provided, it is played directly. It is possible to omit the message and the satellite will not show any text. + If preannounce is True, a sound is played before the announcement. If preannounce_media_id is provided, it overrides the default sound. - If preannounce_media_id is None, no sound is played. Calls async_announce with message and media id. """ @@ -201,7 +202,9 @@ class AssistSatelliteEntity(entity.Entity): message = "" announcement = await self._resolve_announcement_media_id( - message, media_id, preannounce_media_id + message, + media_id, + preannounce_media_id=preannounce_media_id if preannounce else None, ) if self._is_announcing: @@ -229,7 +232,8 @@ class AssistSatelliteEntity(entity.Entity): start_message: str | None = None, start_media_id: str | None = None, extra_system_prompt: str | None = None, - preannounce_media_id: str | None = PREANNOUNCE_URL, + preannounce: bool = True, + preannounce_media_id: str = PREANNOUNCE_URL, ) -> None: """Start a conversation from the satellite. @@ -239,8 +243,8 @@ class AssistSatelliteEntity(entity.Entity): If start_media_id is provided, it is played directly. It is possible to omit the message and the satellite will not show any text. - If preannounce_media_id is provided, it is played before the announcement. - If preannounce_media_id is None, no sound is played. + If preannounce is True, a sound is played before the start message or media. + If preannounce_media_id is provided, it overrides the default sound. Calls async_start_conversation. """ @@ -257,7 +261,9 @@ class AssistSatelliteEntity(entity.Entity): start_message = "" announcement = await self._resolve_announcement_media_id( - start_message, start_media_id, preannounce_media_id + start_message, + start_media_id, + preannounce_media_id=preannounce_media_id if preannounce else None, ) if self._is_announcing: diff --git a/homeassistant/components/assist_satellite/services.yaml b/homeassistant/components/assist_satellite/services.yaml index 7d334d6a8db..d88710c4c4e 100644 --- a/homeassistant/components/assist_satellite/services.yaml +++ b/homeassistant/components/assist_satellite/services.yaml @@ -15,6 +15,11 @@ announce: required: false selector: text: + preannounce: + required: false + default: true + selector: + boolean: preannounce_media_id: required: false selector: @@ -40,6 +45,11 @@ start_conversation: required: false selector: text: + preannounce: + required: false + default: true + selector: + boolean: preannounce_media_id: required: false selector: diff --git a/homeassistant/components/assist_satellite/strings.json b/homeassistant/components/assist_satellite/strings.json index 2bb61516bca..b69711c7106 100644 --- a/homeassistant/components/assist_satellite/strings.json +++ b/homeassistant/components/assist_satellite/strings.json @@ -24,9 +24,13 @@ "name": "Media ID", "description": "The media ID to announce instead of using text-to-speech." }, + "preannounce": { + "name": "Preannounce", + "description": "Play a sound before the announcement." + }, "preannounce_media_id": { - "name": "Preannounce Media ID", - "description": "The media ID to play before the announcement." + "name": "Preannounce media ID", + "description": "Custom media ID to play before the announcement." } } }, @@ -46,9 +50,13 @@ "name": "Extra system prompt", "description": "Provide background information to the AI about the request." }, + "preannounce": { + "name": "Preannounce", + "description": "Play a sound before the start message or media." + }, "preannounce_media_id": { - "name": "Preannounce Media ID", - "description": "The media ID to play before the start message or media." + "name": "Preannounce media ID", + "description": "Custom media ID to play before the start message or media." } } } diff --git a/homeassistant/components/assist_satellite/websocket_api.py b/homeassistant/components/assist_satellite/websocket_api.py index 0a95880706a..6f8b3d723ad 100644 --- a/homeassistant/components/assist_satellite/websocket_api.py +++ b/homeassistant/components/assist_satellite/websocket_api.py @@ -199,7 +199,7 @@ async def websocket_test_connection( hass.async_create_background_task( satellite.async_internal_announce( media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}", - preannounce_media_id=None, + preannounce=False, ), f"assist_satellite_connection_test_{msg['entity_id']}", ) diff --git a/tests/components/assist_satellite/test_entity.py b/tests/components/assist_satellite/test_entity.py index 2b1cc78943f..8050b23f5ff 100644 --- a/tests/components/assist_satellite/test_entity.py +++ b/tests/components/assist_satellite/test_entity.py @@ -186,7 +186,7 @@ async def test_new_pipeline_cancels_pipeline( ("service_data", "expected_params"), [ ( - {"message": "Hello", "preannounce_media_id": None}, + {"message": "Hello", "preannounce": False}, AssistSatelliteAnnouncement( message="Hello", media_id="http://10.10.10.10:8123/api/tts_proxy/test-token", @@ -199,7 +199,7 @@ async def test_new_pipeline_cancels_pipeline( { "message": "Hello", "media_id": "media-source://given", - "preannounce_media_id": None, + "preannounce": False, }, AssistSatelliteAnnouncement( message="Hello", @@ -210,7 +210,7 @@ async def test_new_pipeline_cancels_pipeline( ), ), ( - {"media_id": "http://example.com/bla.mp3", "preannounce_media_id": None}, + {"media_id": "http://example.com/bla.mp3", "preannounce": False}, AssistSatelliteAnnouncement( message="", media_id="http://example.com/bla.mp3", @@ -541,7 +541,7 @@ async def test_vad_sensitivity_entity_not_found( { "start_message": "Hello", "extra_system_prompt": "Better system prompt", - "preannounce_media_id": None, + "preannounce": False, }, ( "mock-conversation-id", @@ -559,7 +559,7 @@ async def test_vad_sensitivity_entity_not_found( { "start_message": "Hello", "start_media_id": "media-source://given", - "preannounce_media_id": None, + "preannounce": False, }, ( "mock-conversation-id", @@ -576,7 +576,7 @@ async def test_vad_sensitivity_entity_not_found( ( { "start_media_id": "http://example.com/given.mp3", - "preannounce_media_id": None, + "preannounce": False, }, ( "mock-conversation-id", diff --git a/tests/components/esphome/test_assist_satellite.py b/tests/components/esphome/test_assist_satellite.py index 5f433a6c0ed..ce5de0a1a67 100644 --- a/tests/components/esphome/test_assist_satellite.py +++ b/tests/components/esphome/test_assist_satellite.py @@ -1252,7 +1252,7 @@ async def test_announce_message( { "entity_id": satellite.entity_id, "message": "test-text", - "preannounce_media_id": None, + "preannounce": False, }, blocking=True, ) @@ -1342,7 +1342,7 @@ async def test_announce_media_id( { "entity_id": satellite.entity_id, "media_id": "https://www.home-assistant.io/resolved.mp3", - "preannounce_media_id": None, + "preannounce": False, }, blocking=True, ) @@ -1553,7 +1553,7 @@ async def test_start_conversation_message( { "entity_id": satellite.entity_id, "start_message": "test-text", - "preannounce_media_id": None, + "preannounce": False, }, blocking=True, ) @@ -1662,7 +1662,7 @@ async def test_start_conversation_media_id( { "entity_id": satellite.entity_id, "start_media_id": "https://www.home-assistant.io/resolved.mp3", - "preannounce_media_id": None, + "preannounce": False, }, blocking=True, ) From 12c4152dbea31827f7e0c06418da6a56e2c5ac62 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 31 Mar 2025 19:08:21 +0200 Subject: [PATCH 110/258] Bump aiowebdav2 to 0.4.5 (#141934) --- homeassistant/components/webdav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webdav/manifest.json b/homeassistant/components/webdav/manifest.json index 260c569b72b..63d093745d1 100644 --- a/homeassistant/components/webdav/manifest.json +++ b/homeassistant/components/webdav/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aiowebdav2"], "quality_scale": "bronze", - "requirements": ["aiowebdav2==0.4.4"] + "requirements": ["aiowebdav2==0.4.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index a12c6dc2444..2610d8f11e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webdav -aiowebdav2==0.4.4 +aiowebdav2==0.4.5 # homeassistant.components.webostv aiowebostv==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd3728b3d88..183cb702df9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webdav -aiowebdav2==0.4.4 +aiowebdav2==0.4.5 # homeassistant.components.webostv aiowebostv==0.7.3 From 649d8638ed3964a41dd62af041810c221601a64b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Mar 2025 18:34:34 +0000 Subject: [PATCH 111/258] Bump version to 2025.4.0b11 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ea9b238e310..fb5592cf1b2 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 = 4 -PATCH_VERSION: Final = "0b10" +PATCH_VERSION: Final = "0b11" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0) diff --git a/pyproject.toml b/pyproject.toml index 97dda40bf6d..e67987d793d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b10" +version = "2025.4.0b11" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 4b3b9ebc29d1ad66d53c4424449d1a54f36b0427 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 31 Mar 2025 21:23:48 +0200 Subject: [PATCH 112/258] Update frontend to 20250331.0 (#141943) --- 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 884436ad4db..ef974177947 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==20250328.0"] + "requirements": ["home-assistant-frontend==20250331.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8172bfb450d..645a2c71f07 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.37.0 hass-nabucasa==0.94.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250328.0 +home-assistant-frontend==20250331.0 home-assistant-intents==2025.3.28 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2610d8f11e2..85cfde750be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250328.0 +home-assistant-frontend==20250331.0 # homeassistant.components.conversation home-assistant-intents==2025.3.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 183cb702df9..1f5e8ac81a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250328.0 +home-assistant-frontend==20250331.0 # homeassistant.components.conversation home-assistant-intents==2025.3.28 From 6e7ac45ac0689b947334c6e10f49ff5ab38bf47e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 31 Mar 2025 21:35:21 +0200 Subject: [PATCH 113/258] Bump async-upnp-client to 0.44.0 (#141946) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 82541476a02..119d1d31d52 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.43.0", "getmac==0.9.5"], + "requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 17fc3dc27e8..0289d5100d6 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -7,7 +7,7 @@ "dependencies": ["ssdp"], "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", - "requirements": ["async-upnp-client==0.43.0"], + "requirements": ["async-upnp-client==0.44.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 6a30efd64f8..5bb69e7f121 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.7.2", "wakeonlan==2.1.0", - "async-upnp-client==0.43.0" + "async-upnp-client==0.44.0" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 6e1fba8c3a3..93943b0a9ea 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.43.0"] + "requirements": ["async-upnp-client==0.44.0"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index df4daa8782c..62ee4ede7d9 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.43.0", "getmac==0.9.5"], + "requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index cf7bc9c9035..07970cb25ca 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -16,7 +16,7 @@ }, "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], - "requirements": ["yeelight==0.7.16", "async-upnp-client==0.43.0"], + "requirements": ["yeelight==0.7.16", "async-upnp-client==0.44.0"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 645a2c71f07..c8e5bb0c549 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ aiozoneinfo==0.2.3 annotatedyaml==0.4.5 astral==2.2 async-interrupt==1.2.2 -async-upnp-client==0.43.0 +async-upnp-client==0.44.0 atomicwrites-homeassistant==1.4.1 attrs==25.1.0 audioop-lts==0.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 85cfde750be..ec1882c5ac0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ asmog==0.0.6 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.43.0 +async-upnp-client==0.44.0 # homeassistant.components.arve asyncarve==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f5e8ac81a0..a9c9298b1d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -478,7 +478,7 @@ arcam-fmj==1.8.1 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.43.0 +async-upnp-client==0.44.0 # homeassistant.components.arve asyncarve==0.1.1 From 427e1abdae1da61e6d4fa210e12199b33032eecb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Mar 2025 20:12:58 +0000 Subject: [PATCH 114/258] Bump version to 2025.4.0b12 --- 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 fb5592cf1b2..8f8ad0cef93 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 = 4 -PATCH_VERSION: Final = "0b11" +PATCH_VERSION: Final = "0b12" __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, 0) diff --git a/pyproject.toml b/pyproject.toml index e67987d793d..7b17e7067d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b11" +version = "2025.4.0b12" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 91438088a0281c4c5d1d89b358a8f2db1874366f Mon Sep 17 00:00:00 2001 From: aaronburt <42388542+aaronburt@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:17:34 +0100 Subject: [PATCH 115/258] Correct unit conversion for OneDrive quota display (#140337) * Correct unit conversion for OneDrive quota display * Convert OneDrive quota values from bytes to GiB in coordinator and update strings --- homeassistant/components/onedrive/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onedrive/coordinator.py b/homeassistant/components/onedrive/coordinator.py index 7b2dbaab87a..3eb7d762712 100644 --- a/homeassistant/components/onedrive/coordinator.py +++ b/homeassistant/components/onedrive/coordinator.py @@ -88,8 +88,8 @@ class OneDriveUpdateCoordinator(DataUpdateCoordinator[Drive]): ), translation_key=key, translation_placeholders={ - "total": str(drive.quota.total), - "used": str(drive.quota.used), + "total": f"{drive.quota.total / (1024**3):.2f}", + "used": f"{drive.quota.used / (1024**3):.2f}", }, ) return drive From 51073c948c804e7f7cc04cf2a3850e6f5bb9b18b Mon Sep 17 00:00:00 2001 From: Mikko Koo <43339021+98ultimate@users.noreply.github.com> Date: Tue, 1 Apr 2025 20:45:23 +0300 Subject: [PATCH 116/258] Fix nordpool Not to return Unknown if price is exactly 0 (#140647) * now the price will return even if it is exactly 0 * now the price will return even if it is exactly 0 * now the price will return even if it is exactly 0 * clean code * clean code * update testing code coverage * change zero testing to SE4 * remove row duplicate * fix date comments * improve testing * simplify if-return-0 * remove unnecessary tests * order testing rows * restore test_sensor_no_next_price * remove_average_price_test * fix test name --- homeassistant/components/nordpool/sensor.py | 2 +- .../nordpool/fixtures/delivery_period_today.json | 2 +- .../nordpool/snapshots/test_diagnostics.ambr | 2 +- .../components/nordpool/snapshots/test_sensor.ambr | 8 ++++---- tests/components/nordpool/test_sensor.py | 13 +++++++++++++ 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nordpool/sensor.py b/homeassistant/components/nordpool/sensor.py index c6993826239..4bde12afc3c 100644 --- a/homeassistant/components/nordpool/sensor.py +++ b/homeassistant/components/nordpool/sensor.py @@ -34,7 +34,7 @@ def validate_prices( index: int, ) -> float | None: """Validate and return.""" - if result := func(entity)[area][index]: + if (result := func(entity)[area][index]) is not None: return result / 1000 return None diff --git a/tests/components/nordpool/fixtures/delivery_period_today.json b/tests/components/nordpool/fixtures/delivery_period_today.json index 77d51dc9433..df48c32a9a9 100644 --- a/tests/components/nordpool/fixtures/delivery_period_today.json +++ b/tests/components/nordpool/fixtures/delivery_period_today.json @@ -162,7 +162,7 @@ "deliveryEnd": "2024-11-05T19:00:00Z", "entryPerArea": { "SE3": 1011.77, - "SE4": 1804.46 + "SE4": 0.0 } }, { diff --git a/tests/components/nordpool/snapshots/test_diagnostics.ambr b/tests/components/nordpool/snapshots/test_diagnostics.ambr index 76a3dd96405..d7f7c4041cd 100644 --- a/tests/components/nordpool/snapshots/test_diagnostics.ambr +++ b/tests/components/nordpool/snapshots/test_diagnostics.ambr @@ -519,7 +519,7 @@ 'deliveryStart': '2024-11-05T18:00:00Z', 'entryPerArea': dict({ 'SE3': 1011.77, - 'SE4': 1804.46, + 'SE4': 0.0, }), }), dict({ diff --git a/tests/components/nordpool/snapshots/test_sensor.ambr b/tests/components/nordpool/snapshots/test_sensor.ambr index 86aa49357c5..be2b04cc520 100644 --- a/tests/components/nordpool/snapshots/test_sensor.ambr +++ b/tests/components/nordpool/snapshots/test_sensor.ambr @@ -1332,7 +1332,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.80446', + 'state': '0.0', }) # --- # name: test_sensor[sensor.nord_pool_se4_daily_average-entry] @@ -1580,9 +1580,9 @@ # name: test_sensor[sensor.nord_pool_se4_lowest_price-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'end': '2024-11-05T03:00:00+00:00', + 'end': '2024-11-05T19:00:00+00:00', 'friendly_name': 'Nord Pool SE4 Lowest price', - 'start': '2024-11-05T02:00:00+00:00', + 'start': '2024-11-05T18:00:00+00:00', 'unit_of_measurement': 'SEK/kWh', }), 'context': , @@ -1590,7 +1590,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.06519', + 'state': '0.0', }) # --- # name: test_sensor[sensor.nord_pool_se4_next_price-entry] diff --git a/tests/components/nordpool/test_sensor.py b/tests/components/nordpool/test_sensor.py index 60be1ee3258..082684a2a02 100644 --- a/tests/components/nordpool/test_sensor.py +++ b/tests/components/nordpool/test_sensor.py @@ -33,6 +33,19 @@ async def test_sensor( await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) +@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_current_price_is_0( + hass: HomeAssistant, load_int: ConfigEntry +) -> None: + """Test the Nord Pool sensor working if price is 0.""" + + current_price = hass.states.get("sensor.nord_pool_se4_current_price") + + assert current_price is not None + assert current_price.state == "0.0" # SE4 2024-11-05T18:00:00Z + + @pytest.mark.freeze_time("2024-11-05T23:00:00+00:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_no_next_price(hass: HomeAssistant, load_int: ConfigEntry) -> None: From fee152654def3dc06aeb9ac802bd45003da41d08 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Tue, 1 Apr 2025 14:41:24 +0200 Subject: [PATCH 117/258] Use saved volume when selecting preset in bluesound integration (#141079) * Use load_preset to select preset as source * Add tests * Fix --------- Co-authored-by: Joostlek --- .../components/bluesound/media_player.py | 12 ++++---- tests/components/bluesound/conftest.py | 4 +-- .../components/bluesound/test_media_player.py | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 135d1b5d27e..0addcc1daac 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -501,18 +501,16 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity return # presets and inputs might have the same name; presets have priority - url: str | None = None for input_ in self._inputs: if input_.text == source: - url = input_.url + await self._player.play_url(input_.url) + return for preset in self._presets: if preset.name == source: - url = preset.url + await self._player.load_preset(preset.id) + return - if url is None: - raise ServiceValidationError(f"Source {source} not found") - - await self._player.play_url(url) + raise ServiceValidationError(f"Source {source} not found") async def async_clear_playlist(self) -> None: """Clear players playlist.""" diff --git a/tests/components/bluesound/conftest.py b/tests/components/bluesound/conftest.py index 717c9f61850..63597ed0532 100644 --- a/tests/components/bluesound/conftest.py +++ b/tests/components/bluesound/conftest.py @@ -102,8 +102,8 @@ class PlayerMockData: ) player.presets = AsyncMock( return_value=[ - Preset("preset1", "1", "url1", "image1", None), - Preset("preset2", "2", "url2", "image2", None), + Preset("preset1", 1, "url1", "image1", None), + Preset("preset2", 2, "url2", "image2", None), ] ) diff --git a/tests/components/bluesound/test_media_player.py b/tests/components/bluesound/test_media_player.py index ed537d0bc57..dcff33399f5 100644 --- a/tests/components/bluesound/test_media_player.py +++ b/tests/components/bluesound/test_media_player.py @@ -17,12 +17,14 @@ from homeassistant.components.bluesound.media_player import ( SERVICE_SET_TIMER, ) from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, ATTR_MEDIA_VOLUME_LEVEL, DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_SELECT_SOURCE, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, @@ -119,6 +121,32 @@ async def test_volume_down( player_mocks.player_data.player.volume.assert_called_once_with(level=9) +async def test_select_input_source( + hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks +) -> None: + """Test the media player select input source.""" + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: "media_player.player_name1111", ATTR_INPUT_SOURCE: "input1"}, + ) + + player_mocks.player_data.player.play_url.assert_called_once_with("url1") + + +async def test_select_preset_source( + hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks +) -> None: + """Test the media player select preset source.""" + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: "media_player.player_name1111", ATTR_INPUT_SOURCE: "preset1"}, + ) + + player_mocks.player_data.player.load_preset.assert_called_once_with(1) + + async def test_attributes_set( hass: HomeAssistant, setup_config_entry: None, From 3951c2ea6665d5f9579c2361fecb456f6e68ade1 Mon Sep 17 00:00:00 2001 From: Ben Jones Date: Tue, 1 Apr 2025 10:16:22 +1300 Subject: [PATCH 118/258] Handle empty or missing state values for MQTT light entities using 'template' schema (#141177) * check for empty or missing values when processing state messages for MQTT light entities using 'template' schema * normalise warning logs * add tests (one is still failing and I can't work out why) * fix test * improve test coverage after PR review * improve test coverage after PR review --- .../components/mqtt/light/schema_template.py | 159 ++++++++++++------ tests/components/mqtt/test_light_template.py | 106 ++++++++++++ 2 files changed, 217 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 595f072416b..f561f15fb51 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -62,6 +62,7 @@ from ..entity import MqttEntity from ..models import ( MqttCommandTemplate, MqttValueTemplate, + PayloadSentinel, PublishPayloadType, ReceiveMessage, ) @@ -126,7 +127,9 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): _command_templates: dict[ str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType] ] - _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] + _value_templates: dict[ + str, Callable[[ReceivePayloadType, ReceivePayloadType], ReceivePayloadType] + ] _fixed_color_mode: ColorMode | str | None _topics: dict[str, str | None] @@ -203,73 +206,133 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): @callback def _state_received(self, msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - state = self._value_templates[CONF_STATE_TEMPLATE](msg.payload) - if state == STATE_ON: + state_value = self._value_templates[CONF_STATE_TEMPLATE]( + msg.payload, + PayloadSentinel.NONE, + ) + if not state_value: + _LOGGER.debug( + "Ignoring message from '%s' with empty state value", msg.topic + ) + elif state_value == STATE_ON: self._attr_is_on = True - elif state == STATE_OFF: + elif state_value == STATE_OFF: self._attr_is_on = False - elif state == PAYLOAD_NONE: + elif state_value == PAYLOAD_NONE: self._attr_is_on = None else: - _LOGGER.warning("Invalid state value received") + _LOGGER.warning( + "Invalid state value '%s' received from %s", + state_value, + msg.topic, + ) if CONF_BRIGHTNESS_TEMPLATE in self._config: - try: - if brightness := int( - self._value_templates[CONF_BRIGHTNESS_TEMPLATE](msg.payload) - ): - self._attr_brightness = brightness - else: - _LOGGER.debug( - "Ignoring zero brightness value for entity %s", - self.entity_id, + brightness_value = self._value_templates[CONF_BRIGHTNESS_TEMPLATE]( + msg.payload, + PayloadSentinel.NONE, + ) + if not brightness_value: + _LOGGER.debug( + "Ignoring message from '%s' with empty brightness value", + msg.topic, + ) + else: + try: + if brightness := int(brightness_value): + self._attr_brightness = brightness + else: + _LOGGER.debug( + "Ignoring zero brightness value for entity %s", + self.entity_id, + ) + except ValueError: + _LOGGER.warning( + "Invalid brightness value '%s' received from %s", + brightness_value, + msg.topic, ) - except ValueError: - _LOGGER.warning("Invalid brightness value received from %s", msg.topic) - if CONF_COLOR_TEMP_TEMPLATE in self._config: - try: - color_temp = self._value_templates[CONF_COLOR_TEMP_TEMPLATE]( - msg.payload + color_temp_value = self._value_templates[CONF_COLOR_TEMP_TEMPLATE]( + msg.payload, + PayloadSentinel.NONE, + ) + if not color_temp_value: + _LOGGER.debug( + "Ignoring message from '%s' with empty color temperature value", + msg.topic, ) - self._attr_color_temp_kelvin = ( - int(color_temp) - if self._color_temp_kelvin - else color_util.color_temperature_mired_to_kelvin(int(color_temp)) - if color_temp != "None" - else None - ) - except ValueError: - _LOGGER.warning("Invalid color temperature value received") + else: + try: + self._attr_color_temp_kelvin = ( + int(color_temp_value) + if self._color_temp_kelvin + else color_util.color_temperature_mired_to_kelvin( + int(color_temp_value) + ) + if color_temp_value != "None" + else None + ) + except ValueError: + _LOGGER.warning( + "Invalid color temperature value '%s' received from %s", + color_temp_value, + msg.topic, + ) if ( CONF_RED_TEMPLATE in self._config and CONF_GREEN_TEMPLATE in self._config and CONF_BLUE_TEMPLATE in self._config ): - try: - red = self._value_templates[CONF_RED_TEMPLATE](msg.payload) - green = self._value_templates[CONF_GREEN_TEMPLATE](msg.payload) - blue = self._value_templates[CONF_BLUE_TEMPLATE](msg.payload) - if red == "None" and green == "None" and blue == "None": - self._attr_hs_color = None - else: - self._attr_hs_color = color_util.color_RGB_to_hs( - int(red), int(green), int(blue) - ) + red_value = self._value_templates[CONF_RED_TEMPLATE]( + msg.payload, + PayloadSentinel.NONE, + ) + green_value = self._value_templates[CONF_GREEN_TEMPLATE]( + msg.payload, + PayloadSentinel.NONE, + ) + blue_value = self._value_templates[CONF_BLUE_TEMPLATE]( + msg.payload, + PayloadSentinel.NONE, + ) + if not red_value or not green_value or not blue_value: + _LOGGER.debug( + "Ignoring message from '%s' with empty color value", msg.topic + ) + elif red_value == "None" and green_value == "None" and blue_value == "None": + self._attr_hs_color = None self._update_color_mode() - except ValueError: - _LOGGER.warning("Invalid color value received") + else: + try: + self._attr_hs_color = color_util.color_RGB_to_hs( + int(red_value), int(green_value), int(blue_value) + ) + self._update_color_mode() + except ValueError: + _LOGGER.warning("Invalid color value received from %s", msg.topic) if CONF_EFFECT_TEMPLATE in self._config: - effect = str(self._value_templates[CONF_EFFECT_TEMPLATE](msg.payload)) - if ( - effect_list := self._config[CONF_EFFECT_LIST] - ) and effect in effect_list: - self._attr_effect = effect + effect_value = self._value_templates[CONF_EFFECT_TEMPLATE]( + msg.payload, + PayloadSentinel.NONE, + ) + if not effect_value: + _LOGGER.debug( + "Ignoring message from '%s' with empty effect value", msg.topic + ) + elif (effect_list := self._config[CONF_EFFECT_LIST]) and str( + effect_value + ) in effect_list: + self._attr_effect = str(effect_value) else: - _LOGGER.warning("Unsupported effect value received") + _LOGGER.warning( + "Unsupported effect value '%s' received from %s", + effect_value, + msg.topic, + ) @callback def _prepare_subscribe_topics(self) -> None: diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index b3a1c11c2b6..e2cc801e97d 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -1545,3 +1545,109 @@ async def test_rgb_value_template_fails( "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" in caplog.text ) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "effect_list": ["rainbow", "colorloop"], + "state_topic": "test-topic", + "state_template": "{{ value_json.state }}", + "brightness_template": "{{ value_json.brightness }}", + "color_temp_template": "{{ value_json.color_temp }}", + "red_template": "{{ value_json.color.red }}", + "green_template": "{{ value_json.color.green }}", + "blue_template": "{{ value_json.color.blue }}", + "effect_template": "{{ value_json.effect }}", + }, + ), + ) + ], +) +async def test_state_templates_ignore_missing_values( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test that rendering of MQTT value template ignores missing values.""" + await mqtt_mock_entry() + + # turn on the light + async_fire_mqtt_message(hass, "test-topic", '{"state": "on"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp_kelvin") is None + assert state.attributes.get("effect") is None + + # update brightness and color temperature (with no state) + async_fire_mqtt_message( + hass, "test-topic", '{"brightness": 255, "color_temp": 145}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == ( + 246, + 244, + 255, + ) # temp converted to color + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp_kelvin") == 6896 + assert state.attributes.get("effect") is None + assert state.attributes.get("xy_color") == (0.317, 0.317) # temp converted to color + assert state.attributes.get("hs_color") == ( + 251.249, + 4.253, + ) # temp converted to color + + # update color + async_fire_mqtt_message( + hass, "test-topic", '{"color": {"red": 255, "green": 128, "blue": 64}}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 64) + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority + assert state.attributes.get("effect") is None + + # update brightness + async_fire_mqtt_message(hass, "test-topic", '{"brightness": 128}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 64) + assert state.attributes.get("brightness") == 128 + assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority + assert state.attributes.get("effect") is None + + # update effect + async_fire_mqtt_message(hass, "test-topic", '{"effect": "rainbow"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 64) + assert state.attributes.get("brightness") == 128 + assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority + assert state.attributes.get("effect") == "rainbow" + + # invalid effect + async_fire_mqtt_message(hass, "test-topic", '{"effect": "invalid"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 64) + assert state.attributes.get("brightness") == 128 + assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority + assert state.attributes.get("effect") == "rainbow" + + # turn off the light + async_fire_mqtt_message(hass, "test-topic", '{"state": "off"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp_kelvin") is None + assert state.attributes.get("effect") is None From e23da1a90f300050c60a3d728e36bf96955977d4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 1 Apr 2025 16:29:18 +0200 Subject: [PATCH 119/258] Fix import issues related to onboarding views (#141919) * Fix import issues related to onboarding views * Add ha-intents and numpy to pyproject.toml * Add more requirements to pyproject.toml * Add more requirements to pyproject.toml --- homeassistant/components/onboarding/views.py | 16 ++++++-- homeassistant/package_constraints.txt | 1 + pyproject.toml | 40 ++++++++++++++++++++ requirements.txt | 8 ++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index f0638e72d94..a84aabe9b48 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -31,7 +31,7 @@ from homeassistant.helpers import area_registry as ar from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.translation import async_get_translations -from homeassistant.setup import async_setup_component +from homeassistant.setup import SetupPhases, async_pause_setup, async_setup_component if TYPE_CHECKING: from . import OnboardingData, OnboardingStorage, OnboardingStoreData @@ -60,7 +60,7 @@ async def async_setup( hass.http.register_view(BackupInfoView(data)) hass.http.register_view(RestoreBackupView(data)) hass.http.register_view(UploadBackupView(data)) - setup_cloud_views(hass, data) + await setup_cloud_views(hass, data) class OnboardingView(HomeAssistantView): @@ -430,9 +430,19 @@ class UploadBackupView(BackupOnboardingView, backup_http.UploadBackupView): return await self._post(request) -def setup_cloud_views(hass: HomeAssistant, data: OnboardingStoreData) -> None: +async def setup_cloud_views(hass: HomeAssistant, data: OnboardingStoreData) -> None: """Set up the cloud views.""" + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # Import the cloud integration in an executor to avoid blocking the + # event loop. + def import_cloud() -> None: + """Import the cloud integration.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.cloud import http_api # noqa: F401 + + await hass.async_add_import_executor_job(import_cloud) + # The cloud integration is imported locally to avoid cloud being imported by # bootstrap.py and to avoid circular imports. diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c8e5bb0c549..d3596e1d713 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -45,6 +45,7 @@ ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 mutagen==1.47.0 +numpy==2.2.2 orjson==3.10.16 packaging>=23.1 paho-mqtt==2.1.0 diff --git a/pyproject.toml b/pyproject.toml index 7b17e7067d0..71574a7b461 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,16 +45,41 @@ dependencies = [ "ciso8601==2.3.2", "cronsim==2.6", "fnv-hash-fast==1.4.0", + # ha-ffmpeg is indirectly imported from onboarding via the import chain + # onboarding->cloud->assist_pipeline->tts->ffmpeg. Onboarding needs + # to be setup in stage 0, but we don't want to also promote cloud with all its + # dependencies to stage 0. + "ha-ffmpeg==3.2.2", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration "hass-nabucasa==0.94.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 + # dependencies to stage 0. + "hassil==2.2.3", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", "home-assistant-bluetooth==1.13.1", + # home_assistant_intents is indirectly imported from onboarding via the import chain + # 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.3.28", "ifaddr==0.2.0", "Jinja2==3.1.6", "lru-dict==1.3.0", + # mutagen is indirectly imported from onboarding via the import chain + # onboarding->cloud->assist_pipeline->tts->mutagen. Onboarding needs + # to be setup in stage 0, but we don't want to also promote cloud with all its + # dependencies to stage 0. + "mutagen==1.47.0", + # numpy is indirectly imported from onboarding via the import chain + # onboarding->cloud->alexa->camera->stream->numpy. Onboarding needs + # to be setup in stage 0, but we don't want to also promote cloud with all its + # dependencies to stage 0. + "numpy==2.2.2", "PyJWT==2.10.1", # PyJWT has loose dependency. We want the latest one. "cryptography==44.0.1", @@ -64,7 +89,22 @@ dependencies = [ "orjson==3.10.16", "packaging>=23.1", "psutil-home-assistant==0.0.1", + # pymicro_vad is indirectly imported from onboarding via the import chain + # onboarding->cloud->assist_pipeline->pymicro_vad. Onboarding needs + # to be setup in stage 0, but we don't want to also promote cloud with all its + # dependencies to stage 0. + "pymicro-vad==1.0.1", + # pyspeex-noise is indirectly imported from onboarding via the import chain + # onboarding->cloud->assist_pipeline->pyspeex_noise. Onboarding needs + # to be setup in stage 0, but we don't want to also promote cloud with all its + # dependencies to stage 0. + "pyspeex-noise==1.0.2", "python-slugify==8.0.4", + # PyTurboJPEG is indirectly imported from onboarding via the import chain + # onboarding->cloud->camera->pyturbojpeg. Onboarding needs + # to be setup in stage 0, but we don't want to also promote cloud with all its + # dependencies to stage 0. + "PyTurboJPEG==1.7.5", "PyYAML==6.0.2", "requests==2.32.3", "securetar==2025.2.1", diff --git a/requirements.txt b/requirements.txt index 378240607cf..d551f4f9fc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,12 +22,17 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.4.0 +ha-ffmpeg==3.2.2 hass-nabucasa==0.94.0 +hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 +home-assistant-intents==2025.3.28 ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 +mutagen==1.47.0 +numpy==2.2.2 PyJWT==2.10.1 cryptography==44.0.1 Pillow==11.1.0 @@ -36,7 +41,10 @@ pyOpenSSL==25.0.0 orjson==3.10.16 packaging>=23.1 psutil-home-assistant==0.0.1 +pymicro-vad==1.0.1 +pyspeex-noise==1.0.2 python-slugify==8.0.4 +PyTurboJPEG==1.7.5 PyYAML==6.0.2 requests==2.32.3 securetar==2025.2.1 From b4281961491c4ab7743e7921c3c19fe6fe8874f6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 1 Apr 2025 19:36:14 +0200 Subject: [PATCH 120/258] Improve SmartThings deprecation (#141939) * Improve SmartThings deprecation * Improve SmartThings deprecation --- .../components/smartthings/binary_sensor.py | 141 ++++++++--------- .../components/smartthings/strings.json | 16 +- homeassistant/components/smartthings/util.py | 83 ++++++++++ .../snapshots/test_binary_sensor.ambr | 144 ------------------ .../smartthings/test_binary_sensor.py | 126 +++++++++++++-- 5 files changed, 273 insertions(+), 237 deletions(-) create mode 100644 homeassistant/components/smartthings/util.py diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index bd09f1725d3..75a080975ea 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -7,26 +7,21 @@ from dataclasses import dataclass from pysmartthings import Attribute, Capability, Category, SmartThings, Status -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.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 FullDevice, SmartThingsConfigEntry -from .const import DOMAIN, MAIN +from .const import MAIN from .entity import SmartThingsEntity +from .util import deprecate_entity @dataclass(frozen=True, kw_only=True) @@ -192,24 +187,64 @@ async def async_setup_entry( ) -> None: """Add binary sensors for a config entry.""" entry_data = entry.runtime_data - async_add_entities( - SmartThingsBinarySensor( - entry_data.client, device, description, capability, attribute, component - ) - for device in entry_data.devices.values() - for capability, attribute_map in CAPABILITY_TO_SENSORS.items() - for attribute, description in attribute_map.items() - for component in device.status - if capability in device.status[component] - and ( - component == MAIN - or (description.exists_fn is not None and description.exists_fn(component)) - ) - and ( - not description.category - or get_main_component_category(device) in description.category - ) - ) + entities = [] + + entity_registry = er.async_get(hass) + + for device in entry_data.devices.values(): # pylint: disable=too-many-nested-blocks + for capability, attribute_map in CAPABILITY_TO_SENSORS.items(): + for attribute, description in attribute_map.items(): + for component in device.status: + if ( + capability in device.status[component] + and ( + component == MAIN + or ( + description.exists_fn is not None + and description.exists_fn(component) + ) + ) + and ( + not description.category + or get_main_component_category(device) + in description.category + ) + ): + if ( + component == MAIN + and (issue := description.deprecated_fn(device.status)) + is not None + ): + if deprecate_entity( + hass, + entity_registry, + BINARY_SENSOR_DOMAIN, + f"{device.device.device_id}_{component}_{capability}_{attribute}_{attribute}", + f"deprecated_binary_{issue}", + ): + entities.append( + SmartThingsBinarySensor( + entry_data.client, + device, + description, + capability, + attribute, + component, + ) + ) + continue + entities.append( + SmartThingsBinarySensor( + entry_data.client, + device, + description, + capability, + attribute, + component, + ) + ) + + async_add_entities(entities) class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity): @@ -257,57 +292,3 @@ class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity): self.get_attribute_value(self.capability, self._attribute) == self.entity_description.is_on_key ) - - async def async_added_to_hass(self) -> None: - """Call when entity is added to hass.""" - await super().async_added_to_hass() - if (issue := self.entity_description.deprecated_fn(self.device.status)) is None: - return - 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_binary_{issue}_{self.entity_id}", - breaks_in_ha_version="2025.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_binary_{issue}", - translation_placeholders={ - "entity": 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.""" - await super().async_will_remove_from_hass() - if (issue := self.entity_description.deprecated_fn(self.device.status)) is None: - return - async_delete_issue( - self.hass, DOMAIN, f"deprecated_binary_{issue}_{self.entity_id}" - ) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index dac7b3cf39a..84544835609 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -480,12 +480,20 @@ }, "issues": { "deprecated_binary_valve": { - "title": "Deprecated valve binary sensor detected in some automations or scripts", - "description": "The valve binary sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nA valve entity with controls is available and should be used going forward. Please use the new valve entity in the above automations or scripts to fix this issue." + "title": "Valve binary sensor deprecated", + "description": "The valve binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. A valve entity with controls is available and should be used going forward. Please update your dashboards, templates accordingly and disable the entity to fix this issue." + }, + "deprecated_binary_valve_scripts": { + "title": "[%key:component::smartthings::issues::deprecated_binary_valve::title%]", + "description": "The valve binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. The entity is used in the following automations or scripts:\n{items}\n\nA valve entity with controls is available and should be used going forward. Please use the new valve entity in the above automations or scripts and disable the entity to fix this issue." }, "deprecated_binary_fridge_door": { - "title": "Deprecated refrigerator door binary sensor detected in some automations or scripts", - "description": "The refrigerator door binary sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nSeparate entities for cooler and freezer door are available and should be used going forward. Please use them in the above automations or scripts to fix this issue." + "title": "Refrigerator door binary sensor deprecated", + "description": "The refrigerator door binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. Separate entities for cooler and freezer door are available and should be used going forward. Please update your dashboards, templates accordingly and disable the entity to fix this issue." + }, + "deprecated_binary_fridge_door_scripts": { + "title": "[%key:component::smartthings::issues::deprecated_binary_fridge_door::title%]", + "description": "The refrigerator door binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. The entity is used in the following automations or scripts:\n{items}\n\nSeparate entities for cooler and freezer door are available and should be used going forward. Please use them in the above automations or scripts and disable the entity to fix this issue." }, "deprecated_switch_appliance": { "title": "Deprecated switch detected in some automations or scripts", diff --git a/homeassistant/components/smartthings/util.py b/homeassistant/components/smartthings/util.py new file mode 100644 index 00000000000..b21652ca629 --- /dev/null +++ b/homeassistant/components/smartthings/util.py @@ -0,0 +1,83 @@ +"""Utility functions for SmartThings integration.""" + +from homeassistant.components.automation import automations_with_entity +from homeassistant.components.script import scripts_with_entity +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) + +from .const import DOMAIN + + +def deprecate_entity( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + platform_domain: str, + entity_unique_id: str, + issue_string: str, +) -> bool: + """Create an issue for deprecated entities.""" + if entity_id := entity_registry.async_get_entity_id( + platform_domain, DOMAIN, entity_unique_id + ): + entity_entry = entity_registry.async_get(entity_id) + if not entity_entry: + return False + if entity_entry.disabled: + entity_registry.async_remove(entity_id) + async_delete_issue( + hass, + DOMAIN, + f"{issue_string}_{entity_id}", + ) + return False + translation_key = issue_string + placeholders = { + "entity_id": entity_id, + "entity_name": entity_entry.name or entity_entry.original_name or "Unknown", + } + if items := get_automations_and_scripts_using_entity(hass, entity_id): + translation_key = f"{translation_key}_scripts" + placeholders.update( + { + "items": "\n".join(items), + } + ) + async_create_issue( + hass, + DOMAIN, + f"{issue_string}_{entity_id}", + breaks_in_ha_version="2025.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders=placeholders, + ) + return True + return False + + +def get_automations_and_scripts_using_entity( + hass: HomeAssistant, + entity_id: str, +) -> list[str]: + """Get automations and scripts using an entity.""" + automations = automations_with_entity(hass, entity_id) + scripts = scripts_with_entity(hass, entity_id) + if not automations and not scripts: + return [] + + entity_reg = er.async_get(hass) + return [ + f"- [{item.original_name}](/config/{integration}/edit/{item.unique_id})" + for integration, entities in ( + ("automation", automations), + ("script", scripts), + ) + for entity_id in entities + if (item := entity_reg.async_get(entity_id)) + ] diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index d41c36aea64..2419a154e05 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -713,54 +713,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_ref_normal_000001][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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_contactSensor_contact_contact', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_ref_normal_000001][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_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_freezer_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -857,54 +809,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_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_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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_contactSensor_contact_contact', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'Frigo Door', - }), - 'context': , - 'entity_id': 'binary_sensor.frigo_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_freezer_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2139,54 +2043,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[virtual_valve][binary_sensor.volvo_valve-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.volvo_valve', - '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', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'valve', - 'unique_id': '612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3_main_valve_valve_valve', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[virtual_valve][binary_sensor.volvo_valve-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'opening', - 'friendly_name': 'volvo Valve', - }), - 'context': , - 'entity_id': 'binary_sensor.volvo_valve', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_all_entities[virtual_water_sensor][binary_sensor.asd_moisture-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 517de034613..f7fcde3746f 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -8,8 +8,9 @@ from syrupy import SnapshotAssertion from homeassistant.components import automation, script from homeassistant.components.automation import automations_with_entity +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.script import scripts_with_entity -from homeassistant.components.smartthings import DOMAIN +from homeassistant.components.smartthings import DOMAIN, MAIN from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir @@ -44,7 +45,7 @@ async def test_state_update( """Test state update.""" await setup_integration(hass, mock_config_entry) - assert hass.states.get("binary_sensor.refrigerator_door").state == STATE_OFF + assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_OFF await trigger_update( hass, @@ -53,35 +54,60 @@ async def test_state_update( Capability.CONTACT_SENSOR, Attribute.CONTACT, "open", + component="cooler", ) - assert hass.states.get("binary_sensor.refrigerator_door").state == STATE_ON + assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_ON @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("device_fixture", "issue_string", "entity_id"), + ("device_fixture", "unique_id", "suggested_object_id", "issue_string", "entity_id"), [ - ("virtual_valve", "valve", "binary_sensor.volvo_valve"), - ("da_ref_normal_000001", "fridge_door", "binary_sensor.refrigerator_door"), + ( + "virtual_valve", + f"612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3_{MAIN}_{Capability.VALVE}_{Attribute.VALVE}_{Attribute.VALVE}", + "volvo_valve", + "valve", + "binary_sensor.volvo_valve", + ), + ( + "da_ref_normal_000001", + f"7db87911-7dce-1cf2-7119-b953432a2f09_{MAIN}_{Capability.CONTACT_SENSOR}_{Attribute.CONTACT}_{Attribute.CONTACT}", + "refrigerator_door", + "fridge_door", + "binary_sensor.refrigerator_door", + ), ], ) -async def test_create_issue( +async def test_create_issue_with_items( hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, issue_registry: ir.IssueRegistry, + unique_id: str, + suggested_object_id: str, issue_string: str, entity_id: str, ) -> None: """Test we create an issue when an automation or script is using a deprecated entity.""" issue_id = f"deprecated_binary_{issue_string}_{entity_id}" + entity_entry = entity_registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + DOMAIN, + unique_id, + suggested_object_id=suggested_object_id, + original_name=suggested_object_id, + ) + assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: { + "id": "test", "alias": "test", "trigger": {"platform": "state", "entity_id": entity_id}, "action": { @@ -113,13 +139,95 @@ async def test_create_issue( await setup_integration(hass, mock_config_entry) + assert hass.states.get(entity_id).state == STATE_OFF + 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 - assert issue_registry.async_get_issue(DOMAIN, issue_id) + issue = issue_registry.async_get_issue(DOMAIN, issue_id) + assert issue is not None + assert issue.translation_key == f"deprecated_binary_{issue_string}_scripts" + assert issue.translation_placeholders == { + "entity_id": entity_id, + "entity_name": suggested_object_id, + "items": "- [test](/config/automation/edit/test)\n- [test](/config/script/edit/test)", + } - await hass.config_entries.async_unload(mock_config_entry.entry_id) + entity_registry.async_update_entity( + entity_entry.entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + await hass.config_entries.async_reload(mock_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, issue_id) + assert len(issue_registry.issues) == 0 + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("device_fixture", "unique_id", "suggested_object_id", "issue_string", "entity_id"), + [ + ( + "virtual_valve", + f"612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3_{MAIN}_{Capability.VALVE}_{Attribute.VALVE}_{Attribute.VALVE}", + "volvo_valve", + "valve", + "binary_sensor.volvo_valve", + ), + ( + "da_ref_normal_000001", + f"7db87911-7dce-1cf2-7119-b953432a2f09_{MAIN}_{Capability.CONTACT_SENSOR}_{Attribute.CONTACT}_{Attribute.CONTACT}", + "refrigerator_door", + "fridge_door", + "binary_sensor.refrigerator_door", + ), + ], +) +async def test_create_issue( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, + unique_id: str, + suggested_object_id: str, + issue_string: str, + entity_id: str, +) -> None: + """Test we create an issue when an automation or script is using a deprecated entity.""" + issue_id = f"deprecated_binary_{issue_string}_{entity_id}" + + entity_entry = entity_registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + DOMAIN, + unique_id, + suggested_object_id=suggested_object_id, + original_name=suggested_object_id, + ) + + await setup_integration(hass, mock_config_entry) + + 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}" + assert issue.translation_placeholders == { + "entity_id": entity_id, + "entity_name": suggested_object_id, + } + + entity_registry.async_update_entity( + entity_entry.entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() # Assert the issue is no longer present From a7c43f9b49a11e766f09b7afa26cb2edc06a9885 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:10:24 -0400 Subject: [PATCH 121/258] Reload the ZBT-1 integration on USB state changes (#141287) * Reload the config entry when the ZBT-1 is unplugged * Register the USB event handler globally to react better to re-plugs * Fix existing unit tests * Add an empty `CONFIG_SCHEMA` * Add a unit test * Fix unit tests * Fix unit tests for Linux * Address most review comments * Address remaining review comments --- .../homeassistant_sky_connect/__init__.py | 48 ++++++- .../homeassistant_sky_connect/conftest.py | 10 ++ .../homeassistant_sky_connect/test_init.py | 131 +++++++++++++++++- 3 files changed, 186 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index e8b8c3bb433..1770e902b0f 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -3,19 +3,63 @@ from __future__ import annotations import logging +import os.path from homeassistant.components.homeassistant_hardware.util import guess_firmware_info +from homeassistant.components.usb import USBDevice, async_register_port_event_callback from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType -from .const import DESCRIPTION, DEVICE, FIRMWARE, FIRMWARE_VERSION, PRODUCT +from .const import DESCRIPTION, DEVICE, DOMAIN, FIRMWARE, FIRMWARE_VERSION, PRODUCT _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the ZBT-1 integration.""" + + @callback + def async_port_event_callback( + added: set[USBDevice], removed: set[USBDevice] + ) -> None: + """Handle USB port events.""" + current_entries_by_path = { + entry.data[DEVICE]: entry + for entry in hass.config_entries.async_entries(DOMAIN) + } + + for device in added | removed: + path = device.device + entry = current_entries_by_path.get(path) + + if entry is not None: + _LOGGER.debug( + "Device %r has changed state, reloading config entry %s", + path, + entry, + ) + hass.config_entries.async_schedule_reload(entry.entry_id) + + async_register_port_event_callback(hass, async_port_event_callback) + + return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant SkyConnect config entry.""" + + # Postpone loading the config entry if the device is missing + device_path = entry.data[DEVICE] + if not await hass.async_add_executor_job(os.path.exists, device_path): + raise ConfigEntryNotReady + await hass.config_entries.async_forward_entry_setups(entry, ["update"]) + return True diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index c5bfa4bd609..89ec292d879 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -47,3 +47,13 @@ def mock_zha_get_last_network_settings() -> Generator[None]: AsyncMock(return_value=None), ): yield + + +@pytest.fixture(autouse=True) +def mock_usb_path_exists() -> Generator[None]: + """Mock os.path.exists to allow the ZBT-1 integration to load.""" + with patch( + "homeassistant.components.homeassistant_sky_connect.os.path.exists", + return_value=True, + ): + yield diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index c467a9e0d60..f38ac158e71 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,15 +1,28 @@ """Test the Home Assistant SkyConnect integration.""" +from datetime import timedelta from unittest.mock import patch +import pytest + from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, FirmwareInfo, ) from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.components.usb import USBDevice +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED 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 +from tests.components.usb import ( + async_request_scan, + force_usb_polling_watcher, # noqa: F401 + patch_scanned_serial_ports, +) async def test_config_entry_migration_v2(hass: HomeAssistant) -> None: @@ -58,3 +71,119 @@ async def test_config_entry_migration_v2(hass: HomeAssistant) -> None: } await hass.config_entries.async_unload(config_entry.entry_id) + + +async def test_setup_fails_on_missing_usb_port(hass: HomeAssistant) -> None: + """Test setup failing when the USB port is missing.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id", + data={ + "description": "SkyConnect v1.0", + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "product": "SkyConnect v1.0", + "firmware": "ezsp", + "firmware_version": "7.4.4.0", + }, + version=1, + minor_version=3, + ) + + config_entry.add_to_hass(hass) + + # Set up the config entry + with patch( + "homeassistant.components.homeassistant_sky_connect.os.path.exists" + ) as mock_exists: + mock_exists.return_value = False + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Failed to set up, the device is missing + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + mock_exists.return_value = True + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=30)) + await hass.async_block_till_done(wait_background_tasks=True) + + # Now it's ready + assert config_entry.state is ConfigEntryState.LOADED + + +@pytest.mark.usefixtures("force_usb_polling_watcher") +async def test_usb_device_reactivity(hass: HomeAssistant) -> None: + """Test setting up USB monitoring.""" + assert await async_setup_component(hass, "usb", {"usb": {}}) + + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id", + data={ + "description": "SkyConnect v1.0", + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "product": "SkyConnect v1.0", + "firmware": "ezsp", + "firmware_version": "7.4.4.0", + }, + version=1, + minor_version=3, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_sky_connect.os.path.exists" + ) as mock_exists: + mock_exists.return_value = False + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Failed to set up, the device is missing + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + # Now we make it available but do not wait + mock_exists.return_value = True + + with patch_scanned_serial_ports( + return_value=[ + USBDevice( + device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + vid="10C4", + pid="EA60", + serial_number="3c0ed67c628beb11b1cd64a0f320645d", + manufacturer="Nabu Casa", + description="SkyConnect v1.0", + ) + ], + ): + await async_request_scan(hass) + + # It loads immediately + await hass.async_block_till_done(wait_background_tasks=True) + assert config_entry.state is ConfigEntryState.LOADED + + # Wait for a bit for the USB scan debouncer to cool off + async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5)) + + # Unplug the stick + mock_exists.return_value = False + + with patch_scanned_serial_ports(return_value=[]): + await async_request_scan(hass) + + # The integration has reloaded and is now in a failed state + await hass.async_block_till_done(wait_background_tasks=True) + assert config_entry.state is ConfigEntryState.SETUP_RETRY From 2b9c903429bf1f20b2334d26f7fb8385c68a52f0 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:27:06 -0400 Subject: [PATCH 122/258] Fix data in old SkyConnect integration config entries or delete them (#141959) * Delete old SkyConnect integration config entries * Try migrating, if possible * Do not delete config entries, log a failure --- .../homeassistant_sky_connect/__init__.py | 60 +++++++++- .../homeassistant_sky_connect/config_flow.py | 2 +- .../homeassistant_sky_connect/test_init.py | 111 +++++++++++++++++- 3 files changed, 166 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 1770e902b0f..212826687e1 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -6,14 +6,29 @@ import logging import os.path from homeassistant.components.homeassistant_hardware.util import guess_firmware_info -from homeassistant.components.usb import USBDevice, async_register_port_event_callback +from homeassistant.components.usb import ( + USBDevice, + async_register_port_event_callback, + scan_serial_ports, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DESCRIPTION, DEVICE, DOMAIN, FIRMWARE, FIRMWARE_VERSION, PRODUCT +from .const import ( + DESCRIPTION, + DEVICE, + DOMAIN, + FIRMWARE, + FIRMWARE_VERSION, + MANUFACTURER, + PID, + PRODUCT, + SERIAL_NUMBER, + VID, +) _LOGGER = logging.getLogger(__name__) @@ -73,7 +88,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Migrate old entry.""" _LOGGER.debug( - "Migrating from version %s:%s", config_entry.version, config_entry.minor_version + "Migrating from version %s.%s", config_entry.version, config_entry.minor_version ) if config_entry.version == 1: @@ -108,6 +123,43 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> minor_version=3, ) + if config_entry.minor_version == 3: + # Old SkyConnect config entries were missing keys + if any( + key not in config_entry.data + for key in (VID, PID, MANUFACTURER, PRODUCT, SERIAL_NUMBER) + ): + serial_ports = await hass.async_add_executor_job(scan_serial_ports) + serial_ports_info = {port.device: port for port in serial_ports} + device = config_entry.data[DEVICE] + + if not (usb_info := serial_ports_info.get(device)): + raise HomeAssistantError( + f"USB device {device} is missing, cannot migrate" + ) + + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + VID: usb_info.vid, + PID: usb_info.pid, + MANUFACTURER: usb_info.manufacturer, + PRODUCT: usb_info.description, + DESCRIPTION: usb_info.description, + SERIAL_NUMBER: usb_info.serial_number, + }, + version=1, + minor_version=4, + ) + else: + # Existing entries are migrated by just incrementing the version + hass.config_entries.async_update_entry( + config_entry, + version=1, + minor_version=4, + ) + _LOGGER.debug( "Migration to version %s.%s successful", config_entry.version, diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index d28d74a681c..eb5ea214b3e 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -81,7 +81,7 @@ class HomeAssistantSkyConnectConfigFlow( """Handle a config flow for Home Assistant SkyConnect.""" VERSION = 1 - MINOR_VERSION = 3 + MINOR_VERSION = 4 def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the config flow.""" diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index f38ac158e71..f027a6d2fb8 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -9,7 +9,15 @@ from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, FirmwareInfo, ) -from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.components.homeassistant_sky_connect.const import ( + DESCRIPTION, + DOMAIN, + MANUFACTURER, + PID, + PRODUCT, + SERIAL_NUMBER, + VID, +) from homeassistant.components.usb import USBDevice from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED @@ -57,7 +65,7 @@ async def test_config_entry_migration_v2(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.version == 1 - assert config_entry.minor_version == 3 + assert config_entry.minor_version == 4 assert config_entry.data == { "description": "SkyConnect v1.0", "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", @@ -187,3 +195,102 @@ async def test_usb_device_reactivity(hass: HomeAssistant) -> None: # The integration has reloaded and is now in a failed state await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_bad_config_entry_fixing(hass: HomeAssistant) -> None: + """Test fixing/deleting config entries with bad data.""" + + # Newly-added ZBT-1 + new_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id-9e2adbd75b8beb119fe564a0f320645d", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "9e2adbd75b8beb119fe564a0f320645d", + "manufacturer": "Nabu Casa", + "product": "SkyConnect v1.0", + "firmware": "ezsp", + "firmware_version": "7.4.4.0 (build 123)", + }, + version=1, + minor_version=3, + ) + + new_entry.add_to_hass(hass) + + # Old config entry, without firmware info + old_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id-3c0ed67c628beb11b1cd64a0f320645d", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_3c0ed67c628beb11b1cd64a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", + }, + version=1, + minor_version=1, + ) + + old_entry.add_to_hass(hass) + + # Bad config entry, missing most keys + bad_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id-9f6c4bba657cc9a4f0cea48bc5948562", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9f6c4bba657cc9a4f0cea48bc5948562-if00-port0", + }, + version=1, + minor_version=2, + ) + + bad_entry.add_to_hass(hass) + + # Bad config entry, missing most keys, but fixable since the device is present + fixable_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id-4f5f3b26d59f8714a78b599690741999", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_4f5f3b26d59f8714a78b599690741999-if00-port0", + }, + version=1, + minor_version=2, + ) + + fixable_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_sky_connect.scan_serial_ports", + return_value=[ + USBDevice( + device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_4f5f3b26d59f8714a78b599690741999-if00-port0", + vid="10C4", + pid="EA60", + serial_number="4f5f3b26d59f8714a78b599690741999", + manufacturer="Nabu Casa", + description="SkyConnect v1.0", + ) + ], + ): + await async_setup_component(hass, "homeassistant_sky_connect", {}) + + assert hass.config_entries.async_get_entry(new_entry.entry_id) is not None + assert hass.config_entries.async_get_entry(old_entry.entry_id) is not None + assert hass.config_entries.async_get_entry(fixable_entry.entry_id) is not None + + updated_entry = hass.config_entries.async_get_entry(fixable_entry.entry_id) + assert updated_entry is not None + assert updated_entry.data[VID] == "10C4" + assert updated_entry.data[PID] == "EA60" + assert updated_entry.data[SERIAL_NUMBER] == "4f5f3b26d59f8714a78b599690741999" + assert updated_entry.data[MANUFACTURER] == "Nabu Casa" + assert updated_entry.data[PRODUCT] == "SkyConnect v1.0" + assert updated_entry.data[DESCRIPTION] == "SkyConnect v1.0" + + untouched_bad_entry = hass.config_entries.async_get_entry(bad_entry.entry_id) + assert untouched_bad_entry.minor_version == 3 From fa3832fbd7220044c7514e3d63dabb8c3ee139ac Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 1 Apr 2025 19:22:32 +0200 Subject: [PATCH 123/258] Improve error handling and logging on MQTT update entity state updates when template rederings fails (#141960) --- homeassistant/components/mqtt/update.py | 15 +++++- tests/components/mqtt/test_update.py | 66 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index c4916b5010c..145f0a2562c 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -26,7 +26,7 @@ from . import subscription from .config import DEFAULT_RETAIN, MQTT_RO_SCHEMA from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, PAYLOAD_EMPTY_JSON from .entity import MqttEntity, async_setup_entity_entry_helper -from .models import MqttValueTemplate, ReceiveMessage +from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage from .schemas import MQTT_ENTITY_COMMON_SCHEMA from .util import valid_publish_topic, valid_subscribe_topic @@ -136,7 +136,18 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): @callback def _handle_state_message_received(self, msg: ReceiveMessage) -> None: """Handle receiving state message via MQTT.""" - payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) + payload = self._templates[CONF_VALUE_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + + if payload is PayloadSentinel.DEFAULT: + _LOGGER.warning( + "Unable to process payload '%s' for topic %s, with value template '%s'", + msg.payload, + msg.topic, + self._config.get(CONF_VALUE_TEMPLATE), + ) + return if not payload or payload == PAYLOAD_EMPTY_JSON: _LOGGER.debug( diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index d70d7dd792b..87eb381db03 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -1,6 +1,7 @@ """The tests for mqtt update component.""" import json +from typing import Any from unittest.mock import patch import pytest @@ -225,6 +226,71 @@ async def test_value_template( assert state.attributes.get("latest_version") == "2.0.0" +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": "test/update", + "value_template": ( + "{\"latest_version\":\"{{ value_json['update']['latest_version'] }}\"," + "\"installed_version\":\"{{ value_json['update']['installed_version'] }}\"," + "\"update_percentage\":{{ value_json['update'].get('progress', 'null') }}}" + ), + "name": "Test Update", + } + } + } + ], +) +async def test_errornous_value_template( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that it fetches the given payload with a template or handles the exception.""" + state_topic = "test/update" + await mqtt_mock_entry() + + # Simulate a template redendering error with payload + # without "update" mapping + example_payload: dict[str, Any] = { + "child_lock": "UNLOCK", + "current": 0.02, + "energy": 212.92, + "indicator_mode": "off/on", + "linkquality": 65, + "power": 0, + "power_outage_memory": "off", + "state": "ON", + "voltage": 232, + } + + async_fire_mqtt_message(hass, state_topic, json.dumps(example_payload)) + await hass.async_block_till_done() + assert hass.states.get("update.test_update") is not None + assert "Unable to process payload '" in caplog.text + + # Add update info + example_payload["update"] = { + "latest_version": "2.0.0", + "installed_version": "1.9.0", + "progress": 20, + } + + async_fire_mqtt_message(hass, state_topic, json.dumps(example_payload)) + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state is not None + + assert state.state == STATE_ON + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "2.0.0" + assert state.attributes.get("update_percentage") == 20 + + @pytest.mark.parametrize( "hass_config", [ From ceb177f80ee78358adc864e003e1e64de0b42082 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Mar 2025 20:31:25 -1000 Subject: [PATCH 124/258] Bump aiohttp to 3.11.15 (#141967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changelog: https://github.com/aio-libs/aiohttp/compare/v3.11.14...v3.11.15 fixes #141855 fixes #141146 --- 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 d3596e1d713..c304ded1694 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.2.0 aiohasupervisor==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.11.14 +aiohttp==3.11.15 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 71574a7b461..d243a6a23c0 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.0", - "aiohttp==3.11.14", + "aiohttp==3.11.15", "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 d551f4f9fc8..79979cd6e1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.3.0 -aiohttp==3.11.14 +aiohttp==3.11.15 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 11b0086a019a870a6db2b92607beed6e0faf5801 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 1 Apr 2025 19:20:31 +0200 Subject: [PATCH 125/258] Add LG ThinQ event bus listener to lifecycle hooks (#142006) --- homeassistant/components/lg_thinq/coordinator.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lg_thinq/coordinator.py b/homeassistant/components/lg_thinq/coordinator.py index 513cd27a7b2..9f84c422277 100644 --- a/homeassistant/components/lg_thinq/coordinator.py +++ b/homeassistant/components/lg_thinq/coordinator.py @@ -63,10 +63,12 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # Add a callback to handle core config update. self.unit_system: str | None = None - self.hass.bus.async_listen( - event_type=EVENT_CORE_CONFIG_UPDATE, - listener=self._handle_update_config, - event_filter=self.async_config_update_filter, + self.config_entry.async_on_unload( + self.hass.bus.async_listen( + event_type=EVENT_CORE_CONFIG_UPDATE, + listener=self._handle_update_config, + event_filter=self.async_config_update_filter, + ) ) async def _handle_update_config(self, _: Event) -> None: From 00177c699e5df2152405d195cbc81e0baabbc55c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Apr 2025 19:19:53 +0200 Subject: [PATCH 126/258] Update frontend to 20250401.0 (#142010) --- 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 ef974177947..4cab8375d1b 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==20250331.0"] + "requirements": ["home-assistant-frontend==20250401.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c304ded1694..c9819f6f371 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.37.0 hass-nabucasa==0.94.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250331.0 +home-assistant-frontend==20250401.0 home-assistant-intents==2025.3.28 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ec1882c5ac0..c852c86201f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250331.0 +home-assistant-frontend==20250401.0 # homeassistant.components.conversation home-assistant-intents==2025.3.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9c9298b1d4..c591f0280b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250331.0 +home-assistant-frontend==20250401.0 # homeassistant.components.conversation home-assistant-intents==2025.3.28 From d47ef835d7bc01b17fd098e03a950a98de66acb6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 1 Apr 2025 20:27:34 +0200 Subject: [PATCH 127/258] Fix train to for multiple stations in Trafikverket Train (#142016) --- homeassistant/components/trafikverket_train/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py index eb0a4a45791..fb39e14815e 100644 --- a/homeassistant/components/trafikverket_train/config_flow.py +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -266,7 +266,7 @@ class TVTrainConfigFlow(ConfigFlow, domain=DOMAIN): { CONF_API_KEY: api_key, CONF_FROM: train_from, - CONF_TO: user_input[CONF_TO], + CONF_TO: train_to, CONF_TIME: train_time, CONF_WEEKDAY: train_days, CONF_FILTER_PRODUCT: filter_product, From cd363d48c31ab4886c7581fd46bf231e435a4149 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Apr 2025 19:12:16 +0000 Subject: [PATCH 128/258] Bump version to 2025.4.0b13 --- 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 8f8ad0cef93..196db3cd2af 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 = 4 -PATCH_VERSION: Final = "0b12" +PATCH_VERSION: Final = "0b13" __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, 0) diff --git a/pyproject.toml b/pyproject.toml index d243a6a23c0..3af77a19002 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b12" +version = "2025.4.0b13" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 35513ae0725114985c09b6c0a1a02d4dd4589bf4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Mar 2025 10:03:07 +0100 Subject: [PATCH 129/258] Remove unused mypy ignore from google_generative_ai_conversation (#141549) --- .../components/google_generative_ai_conversation/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py index b413f9c9a62..b7753c21bf9 100644 --- a/homeassistant/components/google_generative_ai_conversation/config_flow.py +++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py @@ -7,7 +7,7 @@ import logging from types import MappingProxyType from typing import Any -from google import genai # type: ignore[attr-defined] +from google import genai from google.genai.errors import APIError, ClientError from requests.exceptions import Timeout import voluptuous as vol From fc66997a368d9d6eb5c8fb5a7e07ac33fb109748 Mon Sep 17 00:00:00 2001 From: Tomek Wasilczyk Date: Wed, 2 Apr 2025 00:07:36 -0700 Subject: [PATCH 130/258] Fix warning about unfinished oauth tasks on shutdown (#141969) * Don't wait for OAuth token task on shutdown To reproduce the warning: 1. Start authentication with integration using OAuth (e.g. SmartThings) 2. When redirected to external login site, just close the page 3. Settings -> Restart Home Assistant * Clarify comment --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/cloud/account_link.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 851d658f8e0..3c3d944d479 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -127,7 +127,11 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement flow_id=flow_id, user_input=tokens ) - self.hass.async_create_task(await_tokens()) + # It's a background task because it should be cancelled on shutdown and there's nothing else + # we can do in such case. There's also no need to wait for this during setup. + self.hass.async_create_background_task( + await_tokens(), name="Awaiting OAuth tokens" + ) return authorize_url From f29444002e9f5efc39a2198bb386d7de8af6185e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:39:25 -0400 Subject: [PATCH 131/258] Skip firmware config flow confirmation if the hardware is in use (#142017) * Auto-confirm the discovery if we detect that the device is already in use * Add a unit test --- .../firmware_config_flow.py | 11 ++++++++ .../test_config_flow.py | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py index 83031587712..1b4840e5a98 100644 --- a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py +++ b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py @@ -33,6 +33,7 @@ from .util import ( OwningIntegration, get_otbr_addon_manager, get_zigbee_flasher_addon_manager, + guess_firmware_info, guess_hardware_owners, probe_silabs_firmware_info, ) @@ -511,6 +512,16 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm a discovery.""" + assert self._device is not None + fw_info = await guess_firmware_info(self.hass, self._device) + + # If our guess for the firmware type is actually running, we can save the user + # an unnecessary confirmation and silently confirm the flow + for owner in fw_info.owners: + if await owner.is_running(self.hass): + self._probed_firmware_info = fw_info + return self._async_flow_finished() + return await self.async_step_pick_firmware() diff --git a/tests/components/homeassistant_hardware/test_config_flow.py b/tests/components/homeassistant_hardware/test_config_flow.py index 32c5a381233..3081c44c681 100644 --- a/tests/components/homeassistant_hardware/test_config_flow.py +++ b/tests/components/homeassistant_hardware/test_config_flow.py @@ -381,6 +381,32 @@ async def test_config_flow_zigbee_skip_step_if_installed(hass: HomeAssistant) -> assert result["step_id"] == "confirm_zigbee" +async def test_config_flow_auto_confirm_if_running(hass: HomeAssistant) -> None: + """Test the config flow skips the confirmation step the hardware is already used.""" + with patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.guess_firmware_info", + return_value=FirmwareInfo( + device=TEST_DEVICE, + firmware_type=ApplicationType.EZSP, + firmware_version="7.4.4.0", + owners=[Mock(is_running=AsyncMock(return_value=True))], + source="guess", + ), + ): + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + # There are no steps, the config entry is automatically created + assert result["type"] is FlowResultType.CREATE_ENTRY + config_entry = result["result"] + assert config_entry.data == { + "firmware": "ezsp", + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, + } + + async def test_config_flow_thread(hass: HomeAssistant) -> None: """Test the config flow.""" result = await hass.config_entries.flow.async_init( From d888c70ff0417a88d5c482edc1bc0bdc82b3df8a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:43:01 -0400 Subject: [PATCH 132/258] Fix entity names for HA hardware firmware update entities (#142029) * Fix entity names for HA hardware firmware update entities * Fix unit tests --- .../components/homeassistant_hardware/update.py | 7 +------ .../components/homeassistant_sky_connect/update.py | 1 - .../components/homeassistant_yellow/strings.json | 2 +- .../components/homeassistant_yellow/update.py | 10 +++++++--- tests/components/homeassistant_hardware/test_update.py | 9 ++++++++- tests/components/homeassistant_yellow/test_update.py | 2 +- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/update.py b/homeassistant/components/homeassistant_hardware/update.py index 960facc81f8..1b0f15ca021 100644 --- a/homeassistant/components/homeassistant_hardware/update.py +++ b/homeassistant/components/homeassistant_hardware/update.py @@ -95,8 +95,7 @@ class BaseFirmwareUpdateEntity( _attr_supported_features = ( UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS ) - # Until this entity can be associated with a device, we must manually name it - _attr_has_entity_name = False + _attr_has_entity_name = True def __init__( self, @@ -195,10 +194,6 @@ class BaseFirmwareUpdateEntity( def _update_attributes(self) -> None: """Recompute the attributes of the entity.""" - - # This entity is not currently associated with a device so we must manually - # give it a name - self._attr_name = f"{self._config_entry.title} Update" self._attr_title = self.entity_description.firmware_name or "Unknown" if ( diff --git a/homeassistant/components/homeassistant_sky_connect/update.py b/homeassistant/components/homeassistant_sky_connect/update.py index 5eaa1e220be..74c28b37eaf 100644 --- a/homeassistant/components/homeassistant_sky_connect/update.py +++ b/homeassistant/components/homeassistant_sky_connect/update.py @@ -168,7 +168,6 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): """SkyConnect firmware update entity.""" bootloader_reset_type = None - _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index ddff5fd9b6d..41c1438b234 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -152,7 +152,7 @@ }, "entity": { "update": { - "firmware": { + "radio_firmware": { "name": "Radio firmware" } } diff --git a/homeassistant/components/homeassistant_yellow/update.py b/homeassistant/components/homeassistant_yellow/update.py index 94989d5c6b6..9531bd456cb 100644 --- a/homeassistant/components/homeassistant_yellow/update.py +++ b/homeassistant/components/homeassistant_yellow/update.py @@ -44,6 +44,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ ] = { ApplicationType.EZSP: FirmwareUpdateEntityDescription( key="radio_firmware", + translation_key="radio_firmware", display_precision=0, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, @@ -55,6 +56,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ ), ApplicationType.SPINEL: FirmwareUpdateEntityDescription( key="radio_firmware", + translation_key="radio_firmware", display_precision=0, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, @@ -65,7 +67,8 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ firmware_name="OpenThread RCP", ), ApplicationType.CPC: FirmwareUpdateEntityDescription( - key="firmware", + key="radio_firmware", + translation_key="radio_firmware", display_precision=0, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, @@ -76,7 +79,8 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ firmware_name="Multiprotocol", ), ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription( - key="firmware", + key="radio_firmware", + translation_key="radio_firmware", display_precision=0, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, @@ -88,6 +92,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ ), None: FirmwareUpdateEntityDescription( key="radio_firmware", + translation_key="radio_firmware", display_precision=0, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, @@ -168,7 +173,6 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): """Yellow firmware update entity.""" bootloader_reset_type = "yellow" # Triggers a GPIO reset - _attr_has_entity_name = True def __init__( self, diff --git a/tests/components/homeassistant_hardware/test_update.py b/tests/components/homeassistant_hardware/test_update.py index 0c351141e12..23d1e546791 100644 --- a/tests/components/homeassistant_hardware/test_update.py +++ b/tests/components/homeassistant_hardware/test_update.py @@ -43,6 +43,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -61,7 +62,7 @@ from tests.common import ( TEST_DOMAIN = "test" TEST_DEVICE = "/dev/serial/by-id/some-unique-serial-device-12345" TEST_FIRMWARE_RELEASES_URL = "https://example.org/firmware" -TEST_UPDATE_ENTITY_ID = "update.test_firmware" +TEST_UPDATE_ENTITY_ID = "update.mock_name_firmware" TEST_MANIFEST = FirmwareManifest( url=URL("https://example.org/firmware"), html_url=URL("https://example.org/release_notes"), @@ -205,6 +206,12 @@ class MockFirmwareUpdateEntity(BaseFirmwareUpdateEntity): """Initialize the mock SkyConnect firmware update entity.""" super().__init__(device, config_entry, update_coordinator, entity_description) self._attr_unique_id = self.entity_description.key + self._attr_device_info = DeviceInfo( + identifiers={(TEST_DOMAIN, "yellow")}, + name="Mock Name", + model="Mock Model", + manufacturer="Mock Manufacturer", + ) # Use the cached firmware info if it exists if self._config_entry.data["firmware"] is not None: diff --git a/tests/components/homeassistant_yellow/test_update.py b/tests/components/homeassistant_yellow/test_update.py index 2cc7b51836c..66404dc2176 100644 --- a/tests/components/homeassistant_yellow/test_update.py +++ b/tests/components/homeassistant_yellow/test_update.py @@ -17,7 +17,7 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -UPDATE_ENTITY_ID = "update.home_assistant_yellow_firmware" +UPDATE_ENTITY_ID = "update.home_assistant_yellow_radio_firmware" async def test_yellow_update_entity(hass: HomeAssistant) -> None: From ec70e8b0cd57036557ba33e9a70d11288a337158 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 2 Apr 2025 07:45:17 +0200 Subject: [PATCH 133/258] Bump ZHA to 0.0.55 (#142031) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4daa2f2aa40..1c2d6556271 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.54"], + "requirements": ["zha==0.0.55"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index c852c86201f..de0ff9b4053 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3152,7 +3152,7 @@ zeroconf==0.146.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.54 +zha==0.0.55 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c591f0280b6..8f7cc142a3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2542,7 +2542,7 @@ zeroconf==0.146.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.54 +zha==0.0.55 # homeassistant.components.zwave_js zwave-js-server-python==0.62.0 From f17003a79cead22ce511c6a9547665edcefa8054 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Apr 2025 19:43:43 -1000 Subject: [PATCH 134/258] Bump aiohttp to 3.11.16 (#142034) changelog: https://github.com/aio-libs/aiohttp/compare/v3.11.15...v3.11.16 --- 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 c9819f6f371..35238d3ae28 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.2.0 aiohasupervisor==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.11.15 +aiohttp==3.11.16 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 3af77a19002..b6f8185ea9b 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.0", - "aiohttp==3.11.15", + "aiohttp==3.11.16", "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 79979cd6e1f..7095fccc964 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.3.0 -aiohttp==3.11.15 +aiohttp==3.11.16 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 1b1bc6af959dde4d5003152c1741ba08e6bdbb37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Apr 2025 22:37:27 -1000 Subject: [PATCH 135/258] Bump bluetooth-data-tools to 1.26.5 (#142045) changelog: https://github.com/Bluetooth-Devices/bluetooth-data-tools/compare/v1.26.1...v1.26.5 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/components/private_ble_device/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index e4257221374..1b2b0e7267b 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,7 +19,7 @@ "bleak-retry-connector==3.9.0", "bluetooth-adapters==0.21.4", "bluetooth-auto-recovery==1.4.5", - "bluetooth-data-tools==1.26.1", + "bluetooth-data-tools==1.26.5", "dbus-fast==2.43.0", "habluetooth==3.37.0" ] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 1896f2109a7..764345710dd 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.26.1", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.26.5", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 270495c8770..b88ef3f029a 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -35,5 +35,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.26.1", "led-ble==1.1.6"] + "requirements": ["bluetooth-data-tools==1.26.5", "led-ble==1.1.6"] } diff --git a/homeassistant/components/private_ble_device/manifest.json b/homeassistant/components/private_ble_device/manifest.json index 810fce41e05..df24f536527 100644 --- a/homeassistant/components/private_ble_device/manifest.json +++ b/homeassistant/components/private_ble_device/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/private_ble_device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.26.1"] + "requirements": ["bluetooth-data-tools==1.26.5"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 35238d3ae28..658fdc55e21 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ bleak-retry-connector==3.9.0 bleak==0.22.3 bluetooth-adapters==0.21.4 bluetooth-auto-recovery==1.4.5 -bluetooth-data-tools==1.26.1 +bluetooth-data-tools==1.26.5 cached-ipaddress==0.10.0 certifi>=2021.5.30 ciso8601==2.3.2 diff --git a/requirements_all.txt b/requirements_all.txt index de0ff9b4053..582c22130b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ bluetooth-auto-recovery==1.4.5 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.26.1 +bluetooth-data-tools==1.26.5 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f7cc142a3b..8b7cf5eba64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -564,7 +564,7 @@ bluetooth-auto-recovery==1.4.5 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.26.1 +bluetooth-data-tools==1.26.5 # homeassistant.components.bond bond-async==0.2.1 From aa4eb89eee768e6f3cb9d9328fa37c09e43e659b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Apr 2025 09:44:23 +0000 Subject: [PATCH 136/258] Bump version to 2025.4.0b14 --- 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 196db3cd2af..e09e1e26e49 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 = 4 -PATCH_VERSION: Final = "0b13" +PATCH_VERSION: Final = "0b14" __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, 0) diff --git a/pyproject.toml b/pyproject.toml index b6f8185ea9b..76b9b61004b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b13" +version = "2025.4.0b14" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 2396fd109075b021d649f85c905f2d60aa5fc9d2 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 2 Apr 2025 06:38:48 -0400 Subject: [PATCH 137/258] Fix weather templates using new style configuration (#136677) --- homeassistant/components/template/weather.py | 59 +++++++++++++++---- tests/components/template/test_weather.py | 62 ++++++++++++++++++++ 2 files changed, 109 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 7f597f1d9a8..86bab6f5ad1 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -135,6 +135,33 @@ WEATHER_SCHEMA = vol.Schema( PLATFORM_SCHEMA = WEATHER_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema) +@callback +def _async_create_template_tracking_entities( + async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + definitions: list[dict], + unique_id_prefix: str | None, +) -> None: + """Create the weather entities.""" + entities = [] + + 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}" + + entities.append( + WeatherTemplate( + hass, + entity_conf, + unique_id, + ) + ) + + async_add_entities(entities) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -142,24 +169,32 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Template weather.""" - if discovery_info and "coordinator" in discovery_info: + if discovery_info is None: + config = rewrite_common_legacy_to_modern_conf(hass, config) + unique_id = config.get(CONF_UNIQUE_ID) + async_add_entities( + [ + WeatherTemplate( + hass, + config, + unique_id, + ) + ] + ) + return + + if "coordinator" in discovery_info: async_add_entities( TriggerWeatherEntity(hass, discovery_info["coordinator"], config) for config in discovery_info["entities"] ) return - config = rewrite_common_legacy_to_modern_conf(hass, config) - unique_id = config.get(CONF_UNIQUE_ID) - - async_add_entities( - [ - WeatherTemplate( - hass, - config, - unique_id, - ) - ] + _async_create_template_tracking_entities( + async_add_entities, + hass, + discovery_info["entities"], + discovery_info["unique_id"], ) diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index 081028b6f5b..5db6a000ccc 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -928,3 +928,65 @@ async def test_trigger_entity_restore_state_fail( state = hass.states.get("weather.test") assert state.state == STATE_UNKNOWN assert state.attributes.get("temperature") is None + + +async def test_new_style_template_state_text(hass: HomeAssistant) -> None: + """Test the state text of a template.""" + assert await async_setup_component( + hass, + "weather", + { + "weather": [ + {"weather": {"platform": "demo"}}, + ] + }, + ) + assert await async_setup_component( + hass, + "template", + { + "template": { + "weather": { + "name": "test", + "attribution_template": "{{ states('sensor.attribution') }}", + "condition_template": "sunny", + "temperature_template": "{{ states('sensor.temperature') | float }}", + "humidity_template": "{{ states('sensor.humidity') | int }}", + "pressure_template": "{{ states('sensor.pressure') }}", + "wind_speed_template": "{{ states('sensor.windspeed') }}", + "wind_bearing_template": "{{ states('sensor.windbearing') }}", + "ozone_template": "{{ states('sensor.ozone') }}", + "visibility_template": "{{ states('sensor.visibility') }}", + "wind_gust_speed_template": "{{ states('sensor.wind_gust_speed') }}", + "cloud_coverage_template": "{{ states('sensor.cloud_coverage') }}", + "dew_point_template": "{{ states('sensor.dew_point') }}", + "apparent_temperature_template": "{{ states('sensor.apparent_temperature') }}", + }, + }, + }, + ) + + for attr, v_attr, value in ( + ( + "sensor.attribution", + ATTR_ATTRIBUTION, + "The custom attribution", + ), + ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), + ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), + ("sensor.pressure", ATTR_WEATHER_PRESSURE, 1000), + ("sensor.windspeed", ATTR_WEATHER_WIND_SPEED, 20), + ("sensor.windbearing", ATTR_WEATHER_WIND_BEARING, 180), + ("sensor.ozone", ATTR_WEATHER_OZONE, 25), + ("sensor.visibility", ATTR_WEATHER_VISIBILITY, 4.6), + ("sensor.wind_gust_speed", ATTR_WEATHER_WIND_GUST_SPEED, 30), + ("sensor.cloud_coverage", ATTR_WEATHER_CLOUD_COVERAGE, 75), + ("sensor.dew_point", ATTR_WEATHER_DEW_POINT, 2.2), + ("sensor.apparent_temperature", ATTR_WEATHER_APPARENT_TEMPERATURE, 25), + ): + hass.states.async_set(attr, value) + await hass.async_block_till_done() + state = hass.states.get("weather.test") + assert state is not None + assert state.state == "sunny" + assert state.attributes.get(v_attr) == value From b38c64783066db3a088983dc969966b71e38bc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 2 Apr 2025 14:07:00 +0100 Subject: [PATCH 138/258] Allow excluding modules from noisy logs check (#142020) * Allow excluding modules from noisy logs check * Cache non-excluded modules; hardcode self module name; optimize call * Address review comments --- homeassistant/util/logging.py | 25 +++++++++++++++++++++---- tests/util/test_logging.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 1e516742bfe..d5dfab7da6c 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -29,16 +29,22 @@ class HomeAssistantQueueListener(logging.handlers.QueueListener): LOG_COUNTS_RESET_INTERVAL = 300 MAX_LOGS_COUNT = 200 + EXCLUDED_LOG_COUNT_MODULES = [ + "homeassistant.components.automation", + "homeassistant.components.script", + "homeassistant.setup", + "homeassistant.util.logging", + ] + _last_reset: float _log_counts: dict[str, int] - _warned_modules: set[str] def __init__( self, queue: SimpleQueue[logging.Handler], *handlers: logging.Handler ) -> None: """Initialize the handler.""" super().__init__(queue, *handlers) - self._warned_modules = set() + self._module_log_count_skip_flags: dict[str, bool] = {} self._reset_counters(time.time()) @override @@ -53,7 +59,11 @@ class HomeAssistantQueueListener(logging.handlers.QueueListener): self._reset_counters(record.created) module_name = record.name - if module_name == __name__ or module_name in self._warned_modules: + + if skip_flag := self._module_log_count_skip_flags.get(module_name): + return + + if skip_flag is None and self._update_skip_flags(module_name): return self._log_counts[module_name] += 1 @@ -66,13 +76,20 @@ class HomeAssistantQueueListener(logging.handlers.QueueListener): module_name, module_count, ) - self._warned_modules.add(module_name) + self._module_log_count_skip_flags[module_name] = True def _reset_counters(self, time_sec: float) -> None: _LOGGER.debug("Resetting log counters") self._last_reset = time_sec self._log_counts = defaultdict(int) + def _update_skip_flags(self, module_name: str) -> bool: + excluded = any( + module_name.startswith(prefix) for prefix in self.EXCLUDED_LOG_COUNT_MODULES + ) + self._module_log_count_skip_flags[module_name] = excluded + return excluded + class HomeAssistantQueueHandler(logging.handlers.QueueHandler): """Process the log in another thread.""" diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index d213a68d7f2..ba473ee0c58 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -160,6 +160,10 @@ async def test_catch_log_exception_catches_and_logs() -> None: @patch("homeassistant.util.logging.HomeAssistantQueueListener.MAX_LOGS_COUNT", 5) +@patch( + "homeassistant.util.logging.HomeAssistantQueueListener.EXCLUDED_LOG_COUNT_MODULES", + ["excluded"], +) @pytest.mark.parametrize( ( "logger1_count", @@ -182,6 +186,7 @@ async def test_noisy_loggers( logging_util.async_activate_log_queue_handler(hass) logger1 = logging.getLogger("noisy1") logger2 = logging.getLogger("noisy2.module") + logger_excluded = logging.getLogger("excluded.module") for _ in range(logger1_count): logger1.info("This is a log") @@ -189,6 +194,9 @@ async def test_noisy_loggers( for _ in range(logger2_count): logger2.info("This is another log") + for _ in range(logging_util.HomeAssistantQueueListener.MAX_LOGS_COUNT + 1): + logger_excluded.info("This log should not trigger a warning") + await empty_log_queue() assert ( @@ -203,6 +211,33 @@ async def test_noisy_loggers( ) == logger2_expected_notices ) + # Ensure that the excluded module did not trigger a warning + assert ( + caplog.text.count("is logging too frequently") + == logger1_expected_notices + logger2_expected_notices + ) + + # close the handler so the queue thread stops + logging.root.handlers[0].close() + + +@patch("homeassistant.util.logging.HomeAssistantQueueListener.MAX_LOGS_COUNT", 1) +async def test_noisy_loggers_ignores_self( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test that the noisy loggers warning does not trigger a warning for its own module.""" + + logging_util.async_activate_log_queue_handler(hass) + logger1 = logging.getLogger("noisy_module1") + logger2 = logging.getLogger("noisy_module2") + logger3 = logging.getLogger("noisy_module3") + + logger1.info("This is a log") + logger2.info("This is a log") + logger3.info("This is a log") + + await empty_log_queue() + assert caplog.text.count("logging too frequently") == 3 # close the handler so the queue thread stops logging.root.handlers[0].close() From 69380c85ca6f1f528fd63f8e8407338beaf9af7c Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 2 Apr 2025 11:48:27 +0200 Subject: [PATCH 139/258] Bump deebot-client to 12.5.0 (#142046) --- 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 acb5b620719..ad8b3ea70a5 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==12.4.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 582c22130b5..fb3777b5cfa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -758,7 +758,7 @@ debugpy==1.8.13 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==12.4.0 +deebot-client==12.5.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b7cf5eba64..b7563d0d82a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -649,7 +649,7 @@ dbus-fast==2.43.0 debugpy==1.8.13 # homeassistant.components.ecovacs -deebot-client==12.4.0 +deebot-client==12.5.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 42d97d348cbf6e104b71b7baa3915bd51d3894df Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 2 Apr 2025 13:54:58 +0200 Subject: [PATCH 140/258] Add Eve brand (#142067) --- homeassistant/brands/eve.json | 5 +++++ homeassistant/generated/integrations.json | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 homeassistant/brands/eve.json diff --git a/homeassistant/brands/eve.json b/homeassistant/brands/eve.json new file mode 100644 index 00000000000..f27c8b3d849 --- /dev/null +++ b/homeassistant/brands/eve.json @@ -0,0 +1,5 @@ +{ + "domain": "eve", + "name": "Eve", + "iot_standards": ["matter"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f19cd3062a4..da4f08f157d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1815,6 +1815,12 @@ } } }, + "eve": { + "name": "Eve", + "iot_standards": [ + "matter" + ] + }, "evergy": { "name": "Evergy", "integration_type": "virtual", From 4e0f5817478715dead2f6dac5547603798c4deb1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 2 Apr 2025 13:56:23 +0200 Subject: [PATCH 141/258] Improve SmartThings sensor deprecation (#142070) * Improve SmartThings sensor deprecation * Improve SmartThings sensor deprecation * Improve SmartThings sensor deprecation --- .../components/smartthings/sensor.py | 147 ++-- .../components/smartthings/strings.json | 6 +- .../smartthings/snapshots/test_sensor.ambr | 831 ------------------ .../smartthings/test_binary_sensor.py | 2 - tests/components/smartthings/test_sensor.py | 187 +++- 5 files changed, 240 insertions(+), 933 deletions(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 424483d9617..346516be480 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -9,9 +9,8 @@ from typing import Any, cast from pysmartthings import Attribute, Capability, ComponentStatus, SmartThings, Status -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, @@ -33,16 +32,12 @@ from homeassistant.const import ( 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 homeassistant.util import dt as dt_util from . import FullDevice, SmartThingsConfigEntry -from .const import DOMAIN, MAIN +from .const import MAIN from .entity import SmartThingsEntity +from .util import deprecate_entity THERMOSTAT_CAPABILITIES = { Capability.TEMPERATURE_MEASUREMENT, @@ -1021,31 +1016,67 @@ async def async_setup_entry( ) -> None: """Add sensors for a config entry.""" entry_data = entry.runtime_data - async_add_entities( - SmartThingsSensor( - entry_data.client, - device, - description, - capability, - attribute, - ) - for device in entry_data.devices.values() - 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]) - ) - ) + entities = [] + + entity_registry = er.async_get(hass) + + 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] + ) + ): + if ( + description.deprecated + and ( + reason := description.deprecated( + device.status[MAIN] + ) + ) + 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, + ) + ) + continue + entities.append( + SmartThingsSensor( + entry_data.client, + device, + description, + capability, + attribute, + ) + ) + + async_add_entities(entities) class SmartThingsSensor(SmartThingsEntity, SensorEntity): @@ -1113,53 +1144,3 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): return [] return [option.lower() for option in options] return super().options - - async def async_added_to_hass(self) -> None: - """Call when entity is added to hass.""" - await super().async_added_to_hass() - if ( - not self.entity_description.deprecated - or (reason := self.entity_description.deprecated(self.device.status[MAIN])) - is None - ): - return - automations = automations_with_entity(self.hass, self.entity_id) - scripts = scripts_with_entity(self.hass, self.entity_id) - if not automations and not scripts: - return - - entity_reg: er.EntityRegistry = er.async_get(self.hass) - items_list = [ - f"- [{item.original_name}](/config/{integration}/edit/{item.unique_id})" - for integration, entities in ( - ("automation", automations), - ("script", scripts), - ) - for entity_id in entities - if (item := entity_reg.async_get(entity_id)) - ] - - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_{reason}_{self.entity_id}", - breaks_in_ha_version="2025.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_{reason}", - translation_placeholders={ - "entity": 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.""" - await super().async_will_remove_from_hass() - if ( - not self.entity_description.deprecated - or (reason := self.entity_description.deprecated(self.device.status[MAIN])) - is None - ): - return - async_delete_issue(self.hass, DOMAIN, f"deprecated_{reason}_{self.entity_id}") diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 84544835609..ebb5d3ddbe2 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -504,8 +504,12 @@ "description": "The switch `{entity}` 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 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." + }, + "deprecated_media_player_scripts": { "title": "Deprecated sensor detected in some automations or scripts", - "description": "The sensor `{entity}` 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 use the new media player entity in the above automations or scripts 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 use them in the above automations or scripts to use the new media player entity and disable the entity to fix this issue." } } } diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 7be10ebac91..8ace345be18 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -8366,182 +8366,6 @@ 'state': '19.0', }) # --- -# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_media_input_source-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'wifi', - 'bluetooth', - 'hdmi1', - 'hdmi2', - 'digital', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.soundbar_media_input_source', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Media input source', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_input_source', - 'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577_main_mediaInputSource_inputSource_inputSource', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_media_input_source-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Soundbar Media input source', - 'options': list([ - 'wifi', - 'bluetooth', - 'hdmi1', - 'hdmi2', - 'digital', - ]), - }), - 'context': , - 'entity_id': 'sensor.soundbar_media_input_source', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'wifi', - }) -# --- -# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_media_playback_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.soundbar_media_playback_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': 'Media playback status', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_playback_status', - 'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577_main_mediaPlayback_playbackStatus_playbackStatus', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_media_playback_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Soundbar Media playback status', - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'context': , - 'entity_id': 'sensor.soundbar_media_playback_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'playing', - }) -# --- -# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_volume-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.soundbar_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': 'Volume', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'audio_volume', - 'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577_main_audioVolume_volume_volume', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_volume-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Soundbar Volume', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.soundbar_volume', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1', - }) -# --- # name: test_all_entities[ikea_kadrilj][sensor.kitchen_ikea_kadrilj_window_blind_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -8591,261 +8415,6 @@ 'state': '37', }) # --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_media_input_source-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.galaxy_home_mini_media_input_source', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Media input source', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_input_source', - 'unique_id': 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_main_mediaInputSource_inputSource_inputSource', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_media_input_source-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Galaxy Home Mini Media input source', - }), - 'context': , - 'entity_id': 'sensor.galaxy_home_mini_media_input_source', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_media_playback_repeat-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.galaxy_home_mini_media_playback_repeat', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Media playback repeat', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_playback_repeat', - 'unique_id': 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_main_mediaPlaybackRepeat_playbackRepeatMode_playbackRepeatMode', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_media_playback_repeat-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Galaxy Home Mini Media playback repeat', - }), - 'context': , - 'entity_id': 'sensor.galaxy_home_mini_media_playback_repeat', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_media_playback_shuffle-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.galaxy_home_mini_media_playback_shuffle', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Media playback shuffle', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_playback_shuffle', - 'unique_id': 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_main_mediaPlaybackShuffle_playbackShuffle_playbackShuffle', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_media_playback_shuffle-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Galaxy Home Mini Media playback shuffle', - }), - 'context': , - 'entity_id': 'sensor.galaxy_home_mini_media_playback_shuffle', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'disabled', - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_media_playback_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.galaxy_home_mini_media_playback_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': 'Media playback status', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_playback_status', - 'unique_id': 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_main_mediaPlayback_playbackStatus_playbackStatus', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_media_playback_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Galaxy Home Mini Media playback status', - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'context': , - 'entity_id': 'sensor.galaxy_home_mini_media_playback_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'stopped', - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_volume-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.galaxy_home_mini_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': 'Volume', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'audio_volume', - 'unique_id': 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_main_audioVolume_volume_volume', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[im_speaker_ai_0001][sensor.galaxy_home_mini_volume-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Galaxy Home Mini Volume', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.galaxy_home_mini_volume', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '52', - }) -# --- # name: test_all_entities[multipurpose_sensor][sensor.deck_door_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -9184,119 +8753,6 @@ 'state': '20', }) # --- -# name: test_all_entities[sonos_player][sensor.elliots_rum_media_playback_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.elliots_rum_media_playback_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': 'Media playback status', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_playback_status', - 'unique_id': 'c85fced9-c474-4a47-93c2-037cc7829536_main_mediaPlayback_playbackStatus_playbackStatus', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sonos_player][sensor.elliots_rum_media_playback_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Elliots Rum Media playback status', - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'context': , - 'entity_id': 'sensor.elliots_rum_media_playback_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'playing', - }) -# --- -# name: test_all_entities[sonos_player][sensor.elliots_rum_volume-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.elliots_rum_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': 'Volume', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'audio_volume', - 'unique_id': 'c85fced9-c474-4a47-93c2-037cc7829536_main_audioVolume_volume_volume', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[sonos_player][sensor.elliots_rum_volume-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Elliots Rum Volume', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.elliots_rum_volume', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '15', - }) -# --- # name: test_all_entities[tplink_p110][sensor.spulmaschine_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -9407,119 +8863,6 @@ 'state': '0.0', }) # --- -# name: test_all_entities[vd_network_audio_002s][sensor.soundbar_living_media_playback_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.soundbar_living_media_playback_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': 'Media playback status', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_playback_status', - 'unique_id': '0d94e5db-8501-2355-eb4f-214163702cac_main_mediaPlayback_playbackStatus_playbackStatus', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[vd_network_audio_002s][sensor.soundbar_living_media_playback_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Soundbar Living Media playback status', - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'context': , - 'entity_id': 'sensor.soundbar_living_media_playback_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'stopped', - }) -# --- -# name: test_all_entities[vd_network_audio_002s][sensor.soundbar_living_volume-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.soundbar_living_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': 'Volume', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'audio_volume', - 'unique_id': '0d94e5db-8501-2355-eb4f-214163702cac_main_audioVolume_volume_volume', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[vd_network_audio_002s][sensor.soundbar_living_volume-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Soundbar Living Volume', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.soundbar_living_volume', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '17', - }) -# --- # name: test_all_entities[vd_sensor_light_2023][sensor.light_sensor_55_the_frame_brightness_intensity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -9571,132 +8914,6 @@ 'state': '2', }) # --- -# name: test_all_entities[vd_stv_2017_k][sensor.tv_samsung_8_series_49_media_input_source-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'digitaltv', - 'hdmi1', - 'hdmi4', - 'hdmi4', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.tv_samsung_8_series_49_media_input_source', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Media input source', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_input_source', - 'unique_id': '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_main_mediaInputSource_inputSource_inputSource', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[vd_stv_2017_k][sensor.tv_samsung_8_series_49_media_input_source-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': '[TV] Samsung 8 Series (49) Media input source', - 'options': list([ - 'digitaltv', - 'hdmi1', - 'hdmi4', - 'hdmi4', - ]), - }), - 'context': , - 'entity_id': 'sensor.tv_samsung_8_series_49_media_input_source', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'hdmi1', - }) -# --- -# name: test_all_entities[vd_stv_2017_k][sensor.tv_samsung_8_series_49_media_playback_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.tv_samsung_8_series_49_media_playback_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': 'Media playback status', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'media_playback_status', - 'unique_id': '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_main_mediaPlayback_playbackStatus_playbackStatus', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[vd_stv_2017_k][sensor.tv_samsung_8_series_49_media_playback_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': '[TV] Samsung 8 Series (49) Media playback status', - 'options': list([ - 'paused', - 'playing', - 'stopped', - 'fast_forwarding', - 'rewinding', - 'buffering', - ]), - }), - 'context': , - 'entity_id': 'sensor.tv_samsung_8_series_49_media_playback_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_all_entities[vd_stv_2017_k][sensor.tv_samsung_8_series_49_tv_channel-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -9791,54 +9008,6 @@ 'state': '', }) # --- -# name: test_all_entities[vd_stv_2017_k][sensor.tv_samsung_8_series_49_volume-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.tv_samsung_8_series_49_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': 'Volume', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'audio_volume', - 'unique_id': '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_main_audioVolume_volume_volume', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[vd_stv_2017_k][sensor.tv_samsung_8_series_49_volume-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': '[TV] Samsung 8 Series (49) Volume', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.tv_samsung_8_series_49_volume', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '13', - }) -# --- # name: test_all_entities[virtual_thermostat][sensor.asd_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index f7fcde3746f..9f9d8d66317 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -60,7 +60,6 @@ async def test_state_update( assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_ON -@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( ("device_fixture", "unique_id", "suggested_object_id", "issue_string", "entity_id"), [ @@ -167,7 +166,6 @@ async def test_create_issue_with_items( assert len(issue_registry.issues) == 0 -@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( ("device_fixture", "unique_id", "suggested_object_id", "issue_string", "entity_id"), [ diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index fe112b3db6b..e90c177bd6d 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -9,8 +9,9 @@ from syrupy import SnapshotAssertion from homeassistant.components import automation, script from homeassistant.components.automation import automations_with_entity from homeassistant.components.script import scripts_with_entity -from homeassistant.components.smartthings.const import DOMAIN -from homeassistant.const import Platform +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.smartthings.const import DOMAIN, MAIN +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component @@ -56,35 +57,80 @@ async def test_state_update( assert hass.states.get("sensor.ac_office_granit_temperature").state == "20" -@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("device_fixture", "entity_id", "translation_key"), + ( + "device_fixture", + "unique_id", + "suggested_object_id", + "issue_string", + "entity_id", + "expected_state", + ), [ - ("hw_q80r_soundbar", "sensor.soundbar_volume", "media_player"), - ("hw_q80r_soundbar", "sensor.soundbar_media_playback_status", "media_player"), - ("hw_q80r_soundbar", "sensor.soundbar_media_input_source", "media_player"), ( - "im_speaker_ai_0001", - "sensor.galaxy_home_mini_media_playback_shuffle", + "vd_stv_2017_k", + f"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_{MAIN}_{Capability.MEDIA_PLAYBACK}_{Attribute.PLAYBACK_STATUS}_{Attribute.PLAYBACK_STATUS}", + "tv_samsung_8_series_49_media_playback_status", "media_player", + "sensor.tv_samsung_8_series_49_media_playback_status", + STATE_UNKNOWN, + ), + ( + "vd_stv_2017_k", + f"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_{MAIN}_{Capability.AUDIO_VOLUME}_{Attribute.VOLUME}_{Attribute.VOLUME}", + "tv_samsung_8_series_49_volume", + "media_player", + "sensor.tv_samsung_8_series_49_volume", + "13", + ), + ( + "vd_stv_2017_k", + f"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_{MAIN}_{Capability.MEDIA_INPUT_SOURCE}_{Attribute.INPUT_SOURCE}_{Attribute.INPUT_SOURCE}", + "tv_samsung_8_series_49_media_input_source", + "media_player", + "sensor.tv_samsung_8_series_49_media_input_source", + "hdmi1", ), ( "im_speaker_ai_0001", - "sensor.galaxy_home_mini_media_playback_repeat", + f"c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_{MAIN}_{Capability.MEDIA_PLAYBACK_REPEAT}_{Attribute.PLAYBACK_REPEAT_MODE}_{Attribute.PLAYBACK_REPEAT_MODE}", + "galaxy_home_mini_media_playback_repeat", "media_player", + "sensor.galaxy_home_mini_media_playback_repeat", + "off", + ), + ( + "im_speaker_ai_0001", + f"c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_{MAIN}_{Capability.MEDIA_PLAYBACK_SHUFFLE}_{Attribute.PLAYBACK_SHUFFLE}_{Attribute.PLAYBACK_SHUFFLE}", + "galaxy_home_mini_media_playback_shuffle", + "media_player", + "sensor.galaxy_home_mini_media_playback_shuffle", + "disabled", ), ], ) -async def test_create_issue( +async def test_create_issue_with_items( hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, issue_registry: ir.IssueRegistry, + unique_id: str, + suggested_object_id: str, + issue_string: str, entity_id: str, - translation_key: str, + expected_state: str, ) -> None: """Test we create an issue when an automation or script is using a deprecated entity.""" - issue_id = f"deprecated_{translation_key}_{entity_id}" + issue_id = f"deprecated_{issue_string}_{entity_id}" + + entity_entry = entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + unique_id, + suggested_object_id=suggested_object_id, + original_name=suggested_object_id, + ) assert await async_setup_component( hass, @@ -123,19 +169,128 @@ async def test_create_issue( await setup_integration(hass, mock_config_entry) + assert hass.states.get(entity_id).state == expected_state + 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_{translation_key}" + assert issue.translation_key == f"deprecated_{issue_string}_scripts" assert issue.translation_placeholders == { - "entity": entity_id, + "entity_id": entity_id, + "entity_name": suggested_object_id, "items": "- [test](/config/automation/edit/test)\n- [test](/config/script/edit/test)", } - await hass.config_entries.async_unload(mock_config_entry.entry_id) + entity_registry.async_update_entity( + entity_entry.entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + await hass.config_entries.async_reload(mock_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, issue_id) + assert len(issue_registry.issues) == 0 + + +@pytest.mark.parametrize( + ( + "device_fixture", + "unique_id", + "suggested_object_id", + "issue_string", + "entity_id", + "expected_state", + ), + [ + ( + "vd_stv_2017_k", + f"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_{MAIN}_{Capability.MEDIA_PLAYBACK}_{Attribute.PLAYBACK_STATUS}_{Attribute.PLAYBACK_STATUS}", + "tv_samsung_8_series_49_media_playback_status", + "media_player", + "sensor.tv_samsung_8_series_49_media_playback_status", + STATE_UNKNOWN, + ), + ( + "vd_stv_2017_k", + f"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_{MAIN}_{Capability.AUDIO_VOLUME}_{Attribute.VOLUME}_{Attribute.VOLUME}", + "tv_samsung_8_series_49_volume", + "media_player", + "sensor.tv_samsung_8_series_49_volume", + "13", + ), + ( + "vd_stv_2017_k", + f"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_{MAIN}_{Capability.MEDIA_INPUT_SOURCE}_{Attribute.INPUT_SOURCE}_{Attribute.INPUT_SOURCE}", + "tv_samsung_8_series_49_media_input_source", + "media_player", + "sensor.tv_samsung_8_series_49_media_input_source", + "hdmi1", + ), + ( + "im_speaker_ai_0001", + f"c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_{MAIN}_{Capability.MEDIA_PLAYBACK_REPEAT}_{Attribute.PLAYBACK_REPEAT_MODE}_{Attribute.PLAYBACK_REPEAT_MODE}", + "galaxy_home_mini_media_playback_repeat", + "media_player", + "sensor.galaxy_home_mini_media_playback_repeat", + "off", + ), + ( + "im_speaker_ai_0001", + f"c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_{MAIN}_{Capability.MEDIA_PLAYBACK_SHUFFLE}_{Attribute.PLAYBACK_SHUFFLE}_{Attribute.PLAYBACK_SHUFFLE}", + "galaxy_home_mini_media_playback_shuffle", + "media_player", + "sensor.galaxy_home_mini_media_playback_shuffle", + "disabled", + ), + ], +) +async def test_create_issue( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, + unique_id: str, + suggested_object_id: str, + issue_string: str, + entity_id: str, + expected_state: 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}" + + entity_entry = entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + unique_id, + suggested_object_id=suggested_object_id, + original_name=suggested_object_id, + ) + + await setup_integration(hass, mock_config_entry) + + 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}" + assert issue.translation_placeholders == { + "entity_id": entity_id, + "entity_name": suggested_object_id, + } + + entity_registry.async_update_entity( + entity_entry.entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() # Assert the issue is no longer present From 02d182239ace9b50d55979764f6b2f6c298fa0e4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 2 Apr 2025 15:33:17 +0200 Subject: [PATCH 142/258] Improve SmartThings switch deprecation (#142072) --- .../components/smartthings/binary_sensor.py | 11 +- homeassistant/components/smartthings/const.py | 11 +- .../components/smartthings/strings.json | 16 +- .../components/smartthings/switch.py | 143 ++--- .../smartthings/snapshots/test_switch.ambr | 517 ------------------ tests/components/smartthings/test_switch.py | 200 ++++++- 6 files changed, 272 insertions(+), 626 deletions(-) diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 75a080975ea..0fe0e7fe919 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -19,7 +19,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import FullDevice, SmartThingsConfigEntry -from .const import MAIN +from .const import INVALID_SWITCH_CATEGORIES, MAIN from .entity import SmartThingsEntity from .util import deprecate_entity @@ -127,14 +127,7 @@ CAPABILITY_TO_SENSORS: dict[ key=Attribute.SWITCH, device_class=BinarySensorDeviceClass.POWER, is_on_key="on", - category={ - Category.CLOTHING_CARE_MACHINE, - Category.COOKTOP, - Category.DISHWASHER, - Category.DRYER, - Category.MICROWAVE, - Category.WASHER, - }, + category=INVALID_SWITCH_CATEGORIES, ) }, Capability.TAMPER_ALERT: { diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index a3ec9a38200..8f27b785688 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -1,6 +1,6 @@ """Constants used by the SmartThings component and platforms.""" -from pysmartthings import Attribute, Capability +from pysmartthings import Attribute, Capability, Category DOMAIN = "smartthings" @@ -109,3 +109,12 @@ SENSOR_ATTRIBUTES_TO_CAPABILITIES: dict[str, str] = { Attribute.WASHER_MODE: Capability.WASHER_MODE, Attribute.WASHER_JOB_STATE: Capability.WASHER_OPERATING_STATE, } + +INVALID_SWITCH_CATEGORIES = { + Category.CLOTHING_CARE_MACHINE, + Category.COOKTOP, + Category.DRYER, + Category.WASHER, + Category.MICROWAVE, + Category.DISHWASHER, +} diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index ebb5d3ddbe2..0f685a3f48e 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -496,12 +496,20 @@ "description": "The refrigerator door binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. The entity is used in the following automations or scripts:\n{items}\n\nSeparate entities for cooler and freezer door are available and should be used going forward. Please use them in the above automations or scripts and disable the entity to fix this issue." }, "deprecated_switch_appliance": { - "title": "Deprecated switch detected in some automations or scripts", - "description": "The switch `{entity}` 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 to fix this issue." + "title": "Appliance switch deprecated", + "description": "The switch {entity_name} (`{entity_id}`) is deprecated because the actions did not work, so it has been replaced with a binary sensor instead.\n\nPlease update your dashboards, templates accordingly and disable the entity to fix this issue." + }, + "deprecated_switch_appliance_scripts": { + "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", + "description": "The switch {entity_name} (`{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." }, "deprecated_switch_media_player": { "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", - "description": "The switch `{entity}` 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 to fix this issue." + "description": "The switch {entity_name} (`{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." + }, + "deprecated_switch_media_player_scripts": { + "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", + "description": "The switch {entity_name} (`{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_media_player": { "title": "Media player sensors deprecated", @@ -509,7 +517,7 @@ }, "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 use them in 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 entity to fix this issue." } } } diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index e5b74de3241..4e62957d3d4 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -5,23 +5,21 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from pysmartthings import Attribute, Capability, Category, Command, SmartThings +from pysmartthings import Attribute, Capability, Command, SmartThings -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.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SwitchEntity, + SwitchEntityDescription, +) 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 FullDevice, SmartThingsConfigEntry -from .const import DOMAIN, MAIN +from .const import INVALID_SWITCH_CATEGORIES, MAIN from .entity import SmartThingsEntity +from .util import deprecate_entity CAPABILITIES = ( Capability.SWITCH_LEVEL, @@ -37,6 +35,12 @@ AC_CAPABILITIES = ( Capability.THERMOSTAT_COOLING_SETPOINT, ) +MEDIA_PLAYER_CAPABILITIES = ( + Capability.AUDIO_MUTE, + Capability.AUDIO_VOLUME, + Capability.MEDIA_PLAYBACK, +) + @dataclass(frozen=True, kw_only=True) class SmartThingsSwitchEntityDescription(SwitchEntityDescription): @@ -92,13 +96,6 @@ async def async_setup_entry( """Add switches for a config entry.""" entry_data = entry.runtime_data entities: list[SmartThingsEntity] = [ - SmartThingsSwitch(entry_data.client, device, SWITCH, Capability.SWITCH) - for device in entry_data.devices.values() - if Capability.SWITCH in device.status[MAIN] - and not any(capability in device.status[MAIN] for capability in CAPABILITIES) - and not all(capability in device.status[MAIN] for capability in AC_CAPABILITIES) - ] - entities.extend( SmartThingsCommandSwitch( entry_data.client, device, @@ -108,7 +105,7 @@ async def async_setup_entry( for device in entry_data.devices.values() for capability, description in CAPABILITY_TO_COMMAND_SWITCHES.items() if capability in device.status[MAIN] - ) + ] entities.extend( SmartThingsSwitch( entry_data.client, @@ -129,6 +126,51 @@ async def async_setup_entry( ) ) ) + entity_registry = er.async_get(hass) + for device in entry_data.devices.values(): + if ( + Capability.SWITCH in device.status[MAIN] + and not any( + capability in device.status[MAIN] for capability in CAPABILITIES + ) + and not all( + capability in device.status[MAIN] for capability in AC_CAPABILITIES + ) + ): + media_player = all( + capability in device.status[MAIN] + for capability in MEDIA_PLAYER_CAPABILITIES + ) + appliance = ( + device.device.components[MAIN].manufacturer_category + in INVALID_SWITCH_CATEGORIES + ) + if media_player or appliance: + issue = "media_player" if media_player else "appliance" + if deprecate_entity( + hass, + entity_registry, + SWITCH_DOMAIN, + f"{device.device.device_id}_{MAIN}_{Capability.SWITCH}_{Attribute.SWITCH}_{Attribute.SWITCH}", + f"deprecated_switch_{issue}", + ): + entities.append( + SmartThingsSwitch( + entry_data.client, + device, + SWITCH, + Capability.SWITCH, + ) + ) + continue + entities.append( + SmartThingsSwitch( + entry_data.client, + device, + SWITCH, + Capability.SWITCH, + ) + ) async_add_entities(entities) @@ -136,7 +178,6 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): """Define a SmartThings switch.""" entity_description: SmartThingsSwitchEntityDescription - created_issue: bool = False def __init__( self, @@ -182,70 +223,6 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): == "on" ) - async def async_added_to_hass(self) -> None: - """Call when entity is added to hass.""" - await super().async_added_to_hass() - media_player = all( - capability in self.device.status[MAIN] - for capability in ( - Capability.AUDIO_MUTE, - Capability.AUDIO_VOLUME, - Capability.MEDIA_PLAYBACK, - ) - ) - if ( - self.entity_description != SWITCH - and self.device.device.components[MAIN].manufacturer_category - not in { - Category.CLOTHING_CARE_MACHINE, - Category.COOKTOP, - Category.DRYER, - Category.WASHER, - Category.MICROWAVE, - Category.DISHWASHER, - } - ) or (self.entity_description != SWITCH and not media_player): - return - automations = automations_with_entity(self.hass, self.entity_id) - scripts = scripts_with_entity(self.hass, self.entity_id) - if not automations and not scripts: - return - - entity_reg: er.EntityRegistry = er.async_get(self.hass) - items_list = [ - f"- [{item.original_name}](/config/{integration}/edit/{item.unique_id})" - for integration, entities in ( - ("automation", automations), - ("script", scripts), - ) - for entity_id in entities - if (item := entity_reg.async_get(entity_id)) - ] - - identifier = "media_player" if media_player else "appliance" - - self.created_issue = True - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_switch_{self.entity_id}", - breaks_in_ha_version="2025.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_switch_{identifier}", - translation_placeholders={ - "entity": 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.""" - await super().async_will_remove_from_hass() - if not self.created_issue: - return - async_delete_issue(self.hass, DOMAIN, f"deprecated_switch_{self.entity_id}") - class SmartThingsCommandSwitch(SmartThingsSwitch): """Define a SmartThings command switch.""" diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index 8c95d2f20fc..d14d4d02aa4 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -46,100 +46,6 @@ 'state': 'on', }) # --- -# name: test_all_entities[da_ks_cooktop_31001][switch.induction_hob-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.induction_hob', - 'has_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': '808dbd84-f357-47e2-a0cd-3b66fa22d584_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_ks_cooktop_31001][switch.induction_hob-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Induction Hob', - }), - 'context': , - 'entity_id': 'switch.induction_hob', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_all_entities[da_ks_microwave_0101x][switch.microwave-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.microwave', - 'has_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': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_ks_microwave_0101x][switch.microwave-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Microwave', - }), - 'context': , - 'entity_id': 'switch.microwave', - '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,147 +187,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_wm_dw_000001][switch.dishwasher-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.dishwasher', - 'has_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': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_wm_dw_000001][switch.dishwasher-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dishwasher', - }), - 'context': , - 'entity_id': 'switch.dishwasher', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_all_entities[da_wm_sc_000001][switch.airdresser-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.airdresser', - 'has_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': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_wm_sc_000001][switch.airdresser-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'AirDresser', - }), - 'context': , - 'entity_id': 'switch.airdresser', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_all_entities[da_wm_wd_000001][switch.dryer-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.dryer', - 'has_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': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_wm_wd_000001][switch.dryer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dryer', - }), - 'context': , - 'entity_id': 'switch.dryer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_all_entities[da_wm_wd_000001][switch.dryer_wrinkle_prevent-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -469,53 +234,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_wm_wd_000001_1][switch.seca_roupa-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.seca_roupa', - 'has_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': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_wm_wd_000001_1][switch.seca_roupa-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Seca-Roupa', - }), - 'context': , - 'entity_id': 'switch.seca_roupa', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_all_entities[da_wm_wd_000001_1][switch.seca_roupa_wrinkle_prevent-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -563,100 +281,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_wm_wm_000001][switch.washer-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.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': None, - 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_wm_wm_000001][switch.washer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Washer', - }), - 'context': , - 'entity_id': 'switch.washer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_all_entities[da_wm_wm_000001_1][switch.washing_machine-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', - 'has_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': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_wm_wm_000001_1][switch.washing_machine-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Washing Machine', - }), - 'context': , - 'entity_id': 'switch.washing_machine', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_all_entities[da_wm_wm_000001_1][switch.washing_machine_bubble_soak-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -751,53 +375,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[hw_q80r_soundbar][switch.soundbar-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.soundbar', - 'has_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': 'afcf3b91-0000-1111-2222-ddff2a0a6577_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[hw_q80r_soundbar][switch.soundbar-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Soundbar', - }), - 'context': , - 'entity_id': 'switch.soundbar', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_all_entities[sensibo_airconditioner_1][switch.office-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -939,53 +516,6 @@ 'state': 'on', }) # --- -# name: test_all_entities[vd_network_audio_002s][switch.soundbar_living-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.soundbar_living', - 'has_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': '0d94e5db-8501-2355-eb4f-214163702cac_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[vd_network_audio_002s][switch.soundbar_living-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Soundbar Living', - }), - 'context': , - 'entity_id': 'switch.soundbar_living', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_all_entities[vd_sensor_light_2023][switch.light_sensor_55_the_frame-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1033,50 +563,3 @@ 'state': 'off', }) # --- -# name: test_all_entities[vd_stv_2017_k][switch.tv_samsung_8_series_49-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.tv_samsung_8_series_49', - 'has_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': '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[vd_stv_2017_k][switch.tv_samsung_8_series_49-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': '[TV] Samsung 8 Series (49)', - }), - 'context': , - 'entity_id': 'switch.tv_samsung_8_series_49', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 2e360ff68e3..a47ecde7e0d 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -126,25 +126,86 @@ async def test_state_update( assert hass.states.get("switch.2nd_floor_hallway").state == STATE_OFF -@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("device_fixture", "entity_id", "translation_key"), + ("device_fixture", "device_id", "suggested_object_id", "issue_string"), [ - ("da_wm_wm_000001", "switch.washer", "deprecated_switch_appliance"), - ("da_wm_wd_000001", "switch.dryer", "deprecated_switch_appliance"), - ("hw_q80r_soundbar", "switch.soundbar", "deprecated_switch_media_player"), + ( + "da_ks_cooktop_31001", + "808dbd84-f357-47e2-a0cd-3b66fa22d584", + "induction_hob", + "appliance", + ), + ( + "da_ks_microwave_0101x", + "2bad3237-4886-e699-1b90-4a51a3d55c8a", + "microwave", + "appliance", + ), + ( + "da_wm_dw_000001", + "f36dc7ce-cac0-0667-dc14-a3704eb5e676", + "dishwasher", + "appliance", + ), + ( + "da_wm_sc_000001", + "b93211bf-9d96-bd21-3b2f-964fcc87f5cc", + "airdresser", + "appliance", + ), + ( + "da_wm_wd_000001", + "02f7256e-8353-5bdd-547f-bd5b1647e01b", + "dryer", + "appliance", + ), + ( + "da_wm_wm_000001", + "f984b91d-f250-9d42-3436-33f09a422a47", + "washer", + "appliance", + ), + ( + "hw_q80r_soundbar", + "afcf3b91-0000-1111-2222-ddff2a0a6577", + "soundbar", + "media_player", + ), + ( + "vd_network_audio_002s", + "0d94e5db-8501-2355-eb4f-214163702cac", + "soundbar_living", + "media_player", + ), + ( + "vd_stv_2017_k", + "4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1", + "tv_samsung_8_series_49", + "media_player", + ), ], ) -async def test_create_issue( +async def test_create_issue_with_items( hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, issue_registry: ir.IssueRegistry, - entity_id: str, - translation_key: str, + device_id: str, + suggested_object_id: str, + issue_string: str, ) -> None: """Test we create an issue when an automation or script is using a deprecated entity.""" - issue_id = f"deprecated_switch_{entity_id}" + entity_id = f"switch.{suggested_object_id}" + issue_id = f"deprecated_switch_{issue_string}_{entity_id}" + + entity_entry = entity_registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + f"{device_id}_{MAIN}_{Capability.SWITCH}_{Attribute.SWITCH}_{Attribute.SWITCH}", + suggested_object_id=suggested_object_id, + original_name=suggested_object_id, + ) assert await async_setup_component( hass, @@ -183,19 +244,134 @@ async def test_create_issue( await setup_integration(hass, mock_config_entry) + assert hass.states.get(entity_id).state in [STATE_OFF, STATE_ON] + 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 == translation_key + assert issue.translation_key == f"deprecated_switch_{issue_string}_scripts" assert issue.translation_placeholders == { - "entity": entity_id, + "entity_id": entity_id, + "entity_name": suggested_object_id, "items": "- [test](/config/automation/edit/test)\n- [test](/config/script/edit/test)", } - await hass.config_entries.async_unload(mock_config_entry.entry_id) + entity_registry.async_update_entity( + entity_entry.entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + await hass.config_entries.async_reload(mock_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, issue_id) + assert len(issue_registry.issues) == 0 + + +@pytest.mark.parametrize( + ("device_fixture", "device_id", "suggested_object_id", "issue_string"), + [ + ( + "da_ks_cooktop_31001", + "808dbd84-f357-47e2-a0cd-3b66fa22d584", + "induction_hob", + "appliance", + ), + ( + "da_ks_microwave_0101x", + "2bad3237-4886-e699-1b90-4a51a3d55c8a", + "microwave", + "appliance", + ), + ( + "da_wm_dw_000001", + "f36dc7ce-cac0-0667-dc14-a3704eb5e676", + "dishwasher", + "appliance", + ), + ( + "da_wm_sc_000001", + "b93211bf-9d96-bd21-3b2f-964fcc87f5cc", + "airdresser", + "appliance", + ), + ( + "da_wm_wd_000001", + "02f7256e-8353-5bdd-547f-bd5b1647e01b", + "dryer", + "appliance", + ), + ( + "da_wm_wm_000001", + "f984b91d-f250-9d42-3436-33f09a422a47", + "washer", + "appliance", + ), + ( + "hw_q80r_soundbar", + "afcf3b91-0000-1111-2222-ddff2a0a6577", + "soundbar", + "media_player", + ), + ( + "vd_network_audio_002s", + "0d94e5db-8501-2355-eb4f-214163702cac", + "soundbar_living", + "media_player", + ), + ( + "vd_stv_2017_k", + "4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1", + "tv_samsung_8_series_49", + "media_player", + ), + ], +) +async def test_create_issue( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, + device_id: str, + suggested_object_id: str, + issue_string: str, +) -> None: + """Test we create an issue when an automation or script is using a deprecated entity.""" + entity_id = f"switch.{suggested_object_id}" + issue_id = f"deprecated_switch_{issue_string}_{entity_id}" + + entity_entry = entity_registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + f"{device_id}_{MAIN}_{Capability.SWITCH}_{Attribute.SWITCH}_{Attribute.SWITCH}", + suggested_object_id=suggested_object_id, + original_name=suggested_object_id, + ) + + await setup_integration(hass, mock_config_entry) + + 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}" + assert issue.translation_placeholders == { + "entity_id": entity_id, + "entity_name": suggested_object_id, + } + + entity_registry.async_update_entity( + entity_entry.entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() # Assert the issue is no longer present From 9e7dfbb857bde12d5c3bb3c2461b340e197deadc Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 2 Apr 2025 15:39:31 +0200 Subject: [PATCH 143/258] Deprecate None effect instead of breaking it for Hue (#142073) * Deprecate effect none instead of breaking it for Hue * add guard for unknown effect value * revert guard * Fix * Add test * Add test * Add test --------- Co-authored-by: Joostlek --- homeassistant/components/hue/strings.json | 6 ++++ homeassistant/components/hue/v2/light.py | 21 +++++++++++ tests/components/hue/test_light_v2.py | 44 +++++++++++++++++++++-- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 6d2e9054c6f..3326dd1043f 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -197,5 +197,11 @@ } } } + }, + "issues": { + "deprecated_effect_none": { + "title": "Light turned on with deprecated effect", + "description": "A light was turned on with the deprecated effect `None`. This has been replaced with `off`. Please update any automations, scenes, or scripts that use this effect." + } } } diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 757b69c7b7b..8eb7ec8936e 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -29,6 +29,7 @@ from homeassistant.components.light import ( 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 @@ -44,6 +45,9 @@ FALLBACK_MIN_KELVIN = 6500 FALLBACK_MAX_KELVIN = 2000 FALLBACK_KELVIN = 5800 # halfway +# HA 2025.4 replaced the deprecated effect "None" with HA default "off" +DEPRECATED_EFFECT_NONE = "None" + async def async_setup_entry( hass: HomeAssistant, @@ -233,6 +237,23 @@ class HueLight(HueBaseEntity, LightEntity): self._color_temp_active = color_temp is not None flash = kwargs.get(ATTR_FLASH) effect = effect_str = kwargs.get(ATTR_EFFECT) + if effect_str == DEPRECATED_EFFECT_NONE: + # deprecated effect "None" is now "off" + effect_str = EFFECT_OFF + async_create_issue( + self.hass, + DOMAIN, + "deprecated_effect_none", + breaks_in_ha_version="2025.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_effect_none", + ) + self.logger.warning( + "Detected deprecated effect 'None' in %s, use 'off' instead. " + "This will stop working in HA 2025.10", + self.entity_id, + ) if effect_str == EFFECT_OFF: # ignore effect if set to "off" and we have no effect active # the special effect "off" is only used to stop an active effect diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 3d323d4d31c..f4a6fcfba93 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -2,9 +2,14 @@ from unittest.mock import Mock -from homeassistant.components.light import ColorMode +from homeassistant.components.light import ( + ATTR_EFFECT, + DOMAIN as LIGHT_DOMAIN, + ColorMode, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON 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 homeassistant.util.json import JsonArrayType from .conftest import setup_platform @@ -639,3 +644,38 @@ async def test_grouped_lights( mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"] == "identify" ) + + +async def test_light_turn_on_service_deprecation( + hass: HomeAssistant, + mock_bridge_v2: Mock, + v2_resources_test_data: JsonArrayType, + issue_registry: ir.IssueRegistry, +) -> None: + """Test calling the turn on service on a light.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + test_light_id = "light.hue_light_with_color_temperature_only" + + await setup_platform(hass, mock_bridge_v2, "light") + + event = { + "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "type": "light", + "effects": {"status": "candle"}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + + # test disable effect + # it should send a request with effect set to "no_effect" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: test_light_id, + ATTR_EFFECT: "None", + }, + blocking=True, + ) + assert mock_bridge_v2.mock_requests[0]["json"]["effects"]["effect"] == "no_effect" From 4f22fe8f7f7b5bc296d684beb8345008a5c63999 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:33:41 -0400 Subject: [PATCH 144/258] Translation key for ZBT-1 integration failing due to disconnection (#142077) Translation key for device disconnected --- .../components/homeassistant_sky_connect/__init__.py | 5 ++++- .../components/homeassistant_sky_connect/strings.json | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 212826687e1..dfc129ddc75 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -71,7 +71,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Postpone loading the config entry if the device is missing device_path = entry.data[DEVICE] if not await hass.async_add_executor_job(os.path.exists, device_path): - raise ConfigEntryNotReady + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="device_disconnected", + ) await hass.config_entries.async_forward_entry_setups(entry, ["update"]) diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index a596b9846ce..a990f025e8d 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -195,5 +195,10 @@ "run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]", "uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]" } + }, + "exceptions": { + "device_disconnected": { + "message": "The device is not plugged in" + } } } From 94558e2d402988fceb1cf70336f04c243ee0b7be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Apr 2025 14:19:49 +0000 Subject: [PATCH 145/258] Bump version to 2025.4.0b15 --- 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 e09e1e26e49..b44a832c3bd 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 = 4 -PATCH_VERSION: Final = "0b14" +PATCH_VERSION: Final = "0b15" __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, 0) diff --git a/pyproject.toml b/pyproject.toml index 76b9b61004b..c36ca7b6a71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b14" +version = "2025.4.0b15" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 05507d77e3e789d4709c1deae2cedbec6934550d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:00:29 +0200 Subject: [PATCH 146/258] Fix state class for battery sensors in AVM Fritz!SmartHome (#142078) * set proper state class for battery sensor * fix tests --- homeassistant/components/fritzbox/sensor.py | 1 + tests/components/fritzbox/test_binary_sensor.py | 8 ++++++-- tests/components/fritzbox/test_climate.py | 8 ++++++-- tests/components/fritzbox/test_sensor.py | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index bed7004bd6a..801a3a67a6e 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -137,6 +137,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, suitable=lambda device: device.battery_level is not None, native_value=lambda device: device.battery_level, diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 594ed14a7d1..d5b0b5d196b 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -11,7 +11,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, ) from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN -from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorStateClass, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -71,7 +75,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: assert state.state == "23" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Battery" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert ATTR_STATE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT async def test_is_off(hass: HomeAssistant, fritz: Mock) -> None: diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 7766d906f68..699a2b8c53e 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -37,7 +37,11 @@ from homeassistant.components.fritzbox.const import ( ATTR_STATE_WINDOW_OPEN, DOMAIN as FB_DOMAIN, ) -from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorStateClass, +) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, @@ -99,7 +103,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: assert state.state == "23" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Battery" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert ATTR_STATE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_comfort_temperature") assert state diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 67b2c3e8ab6..cb136eee993 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -71,7 +71,7 @@ async def test_setup( "23", f"{CONF_FAKE_NAME} Battery", PERCENTAGE, - None, + SensorStateClass.MEASUREMENT, EntityCategory.DIAGNOSTIC, ], ) From 0c19e47bd454b1e7411a3258e960fd1fcd857562 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Apr 2025 15:02:28 +0000 Subject: [PATCH 147/258] Bump version to 2025.4.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 b44a832c3bd..b362b0a61e3 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 = 4 -PATCH_VERSION: Final = "0b15" +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, 0) diff --git a/pyproject.toml b/pyproject.toml index c36ca7b6a71..8966c2fd9b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0b15" +version = "2025.4.0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 39549d5dd41315f79e50482eb4802aac7383ab82 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 2 Apr 2025 17:15:36 +0200 Subject: [PATCH 148/258] Fix switch name Unknown in SmartThings (#142081) Fix switch name Unknown --- homeassistant/components/smartthings/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 0f685a3f48e..5e18dada260 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -497,19 +497,19 @@ }, "deprecated_switch_appliance": { "title": "Appliance switch deprecated", - "description": "The switch {entity_name} (`{entity_id}`) is deprecated because the actions did not work, so it has been replaced with a binary sensor instead.\n\nPlease update your dashboards, templates accordingly 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\nPlease update your dashboards, templates accordingly and disable the entity to fix this issue." }, "deprecated_switch_appliance_scripts": { "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", - "description": "The switch {entity_name} (`{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 entity to fix this issue." }, "deprecated_switch_media_player": { "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", - "description": "The switch {entity_name} (`{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 use the new media player entity in the above automations or scripts and disable the entity to fix this issue." }, "deprecated_switch_media_player_scripts": { "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", - "description": "The switch {entity_name} (`{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 entity to fix this issue." }, "deprecated_media_player": { "title": "Media player sensors deprecated", From 9070a8d5798b6223560643395999906c0ca4e0e0 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 3 Apr 2025 16:54:23 +0200 Subject: [PATCH 149/258] Fix blocking event loop - daikin (#141442) * fix blocking event loop * create ssl_context directly * update manifest * update manifest.json --- homeassistant/components/daikin/__init__.py | 2 ++ homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 0eaffa39ee9..88a7b71e3ed 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -21,6 +21,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.util.ssl import client_context_no_verify from .const import KEY_MAC, TIMEOUT from .coordinator import DaikinConfigEntry, DaikinCoordinator @@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo key=entry.data.get(CONF_API_KEY), uuid=entry.data.get(CONF_UUID), password=entry.data.get(CONF_PASSWORD), + ssl_context=client_context_no_verify(), ) _LOGGER.debug("Connection to %s successful", host) except TimeoutError as err: diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 86fc804ec92..947fe514747 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.14.1"], + "requirements": ["pydaikin==2.15.0"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index fb3777b5cfa..336d8c2110b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1891,7 +1891,7 @@ pycsspeechtts==1.0.8 # pycups==2.0.4 # homeassistant.components.daikin -pydaikin==2.14.1 +pydaikin==2.15.0 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7563d0d82a..e27279ea9db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1548,7 +1548,7 @@ pycountry==24.6.1 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.14.1 +pydaikin==2.15.0 # homeassistant.components.deako pydeako==0.6.0 From 802ad2ff5120ce55099c8cf81ff4174cdd7fabf3 Mon Sep 17 00:00:00 2001 From: Ivan Lopez Hernandez Date: Thu, 3 Apr 2025 13:23:59 -0700 Subject: [PATCH 150/258] Made Google Search enable dependent on Assist availability (#141712) * Made Google Search enable dependent on Assist availability * Show error instead of rendering again * Cleanup test code --- .../config_flow.py | 18 +- .../strings.json | 3 + .../test_config_flow.py | 164 +++++++++++++++--- 3 files changed, 151 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py index b7753c21bf9..ac6cb696a7d 100644 --- a/homeassistant/components/google_generative_ai_conversation/config_flow.py +++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py @@ -179,28 +179,30 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow): ) -> ConfigFlowResult: """Manage the options.""" options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options + errors: dict[str, str] = {} if user_input is not None: if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended: if user_input[CONF_LLM_HASS_API] == "none": user_input.pop(CONF_LLM_HASS_API) - return self.async_create_entry(title="", data=user_input) + if not ( + user_input.get(CONF_LLM_HASS_API, "none") != "none" + and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True + ): + # Don't allow to save options that enable the Google Seearch tool with an Assist API + return self.async_create_entry(title="", data=user_input) + errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option" # Re-render the options again, now with the recommended options shown/hidden self.last_rendered_recommended = user_input[CONF_RECOMMENDED] - options = { - CONF_RECOMMENDED: user_input[CONF_RECOMMENDED], - CONF_PROMPT: user_input[CONF_PROMPT], - CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API], - } + options = user_input schema = await google_generative_ai_config_option_schema( self.hass, options, self._genai_client ) return self.async_show_form( - step_id="init", - data_schema=vol.Schema(schema), + step_id="init", data_schema=vol.Schema(schema), errors=errors ) diff --git a/homeassistant/components/google_generative_ai_conversation/strings.json b/homeassistant/components/google_generative_ai_conversation/strings.json index b814f89469a..cd7af711d3a 100644 --- a/homeassistant/components/google_generative_ai_conversation/strings.json +++ b/homeassistant/components/google_generative_ai_conversation/strings.json @@ -43,6 +43,9 @@ "prompt": "Instruct how the LLM should respond. This can be a template." } } + }, + "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\"." } }, "services": { 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 f7635c0b45e..8fda02b335d 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -39,9 +39,8 @@ from . import CLIENT_ERROR_500, CLIENT_ERROR_API_KEY_INVALID from tests.common import MockConfigEntry -@pytest.fixture -def mock_models(): - """Mock the model list API.""" +def get_models_pager(): + """Return a generator that yields the models.""" model_20_flash = Mock( display_name="Gemini 2.0 Flash", supported_actions=["generateContent"], @@ -72,11 +71,7 @@ def mock_models(): yield model_15_pro yield model_10_pro - with patch( - "google.genai.models.AsyncModels.list", - return_value=models_pager(), - ): - yield + return models_pager() async def test_form(hass: HomeAssistant) -> None: @@ -119,8 +114,13 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +def will_options_be_rendered_again(current_options, new_options) -> bool: + """Determine if options will be rendered again.""" + return current_options.get(CONF_RECOMMENDED) != new_options.get(CONF_RECOMMENDED) + + @pytest.mark.parametrize( - ("current_options", "new_options", "expected_options"), + ("current_options", "new_options", "expected_options", "errors"), [ ( { @@ -147,6 +147,7 @@ async def test_form(hass: HomeAssistant) -> None: CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, CONF_USE_GOOGLE_SEARCH_TOOL: RECOMMENDED_USE_GOOGLE_SEARCH_TOOL, }, + None, ), ( { @@ -157,6 +158,7 @@ async def test_form(hass: HomeAssistant) -> None: CONF_TOP_P: RECOMMENDED_TOP_P, CONF_TOP_K: RECOMMENDED_TOP_K, CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_USE_GOOGLE_SEARCH_TOOL: True, }, { CONF_RECOMMENDED: True, @@ -168,6 +170,98 @@ async def test_form(hass: HomeAssistant) -> None: CONF_LLM_HASS_API: "assist", CONF_PROMPT: "", }, + None, + ), + ( + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_TEMPERATURE: 0.3, + CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, + CONF_TOP_P: RECOMMENDED_TOP_P, + CONF_TOP_K: RECOMMENDED_TOP_K, + CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_USE_GOOGLE_SEARCH_TOOL: RECOMMENDED_USE_GOOGLE_SEARCH_TOOL, + }, + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_TEMPERATURE: 0.3, + CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, + CONF_TOP_P: RECOMMENDED_TOP_P, + CONF_TOP_K: RECOMMENDED_TOP_K, + CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_USE_GOOGLE_SEARCH_TOOL: True, + }, + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_TEMPERATURE: 0.3, + CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, + CONF_TOP_P: RECOMMENDED_TOP_P, + CONF_TOP_K: RECOMMENDED_TOP_K, + CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_USE_GOOGLE_SEARCH_TOOL: True, + }, + None, + ), + ( + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_TEMPERATURE: 0.3, + CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, + CONF_TOP_P: RECOMMENDED_TOP_P, + CONF_TOP_K: RECOMMENDED_TOP_K, + CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_USE_GOOGLE_SEARCH_TOOL: True, + }, + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_LLM_HASS_API: "assist", + CONF_TEMPERATURE: 0.3, + CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, + CONF_TOP_P: RECOMMENDED_TOP_P, + CONF_TOP_K: RECOMMENDED_TOP_K, + CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_USE_GOOGLE_SEARCH_TOOL: True, + }, + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_TEMPERATURE: 0.3, + CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, + CONF_TOP_P: RECOMMENDED_TOP_P, + CONF_TOP_K: RECOMMENDED_TOP_K, + CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD, + CONF_USE_GOOGLE_SEARCH_TOOL: True, + }, + {CONF_USE_GOOGLE_SEARCH_TOOL: "invalid_google_search_option"}, ), ], ) @@ -175,10 +269,10 @@ async def test_form(hass: HomeAssistant) -> None: async def test_options_switching( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_models, current_options, new_options, expected_options, + errors, ) -> None: """Test the options form.""" with patch("google.genai.models.AsyncModels.get"): @@ -186,24 +280,42 @@ async def test_options_switching( mock_config_entry, options=current_options ) await hass.async_block_till_done() - options_flow = await hass.config_entries.options.async_init( - mock_config_entry.entry_id - ) - if current_options.get(CONF_RECOMMENDED) != new_options.get(CONF_RECOMMENDED): - options_flow = await hass.config_entries.options.async_configure( - options_flow["flow_id"], - { - **current_options, - CONF_RECOMMENDED: new_options[CONF_RECOMMENDED], - }, + with patch( + "google.genai.models.AsyncModels.list", + return_value=get_models_pager(), + ): + options_flow = await hass.config_entries.options.async_init( + mock_config_entry.entry_id + ) + if will_options_be_rendered_again(current_options, new_options): + retry_options = { + **current_options, + CONF_RECOMMENDED: new_options[CONF_RECOMMENDED], + } + with patch( + "google.genai.models.AsyncModels.list", + return_value=get_models_pager(), + ): + options_flow = await hass.config_entries.options.async_configure( + options_flow["flow_id"], + retry_options, + ) + with patch( + "google.genai.models.AsyncModels.list", + return_value=get_models_pager(), + ): + options = await hass.config_entries.options.async_configure( + options_flow["flow_id"], + new_options, ) - options = await hass.config_entries.options.async_configure( - options_flow["flow_id"], - new_options, - ) await hass.async_block_till_done() - assert options["type"] is FlowResultType.CREATE_ENTRY - assert options["data"] == expected_options + if errors is None: + assert options["type"] is FlowResultType.CREATE_ENTRY + assert options["data"] == expected_options + + else: + assert options["type"] is FlowResultType.FORM + assert options.get("errors", None) == errors @pytest.mark.parametrize( From 06ebe0810fd0c2efb1bee713f1ed17b90ad7518b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 2 Apr 2025 19:17:51 +0200 Subject: [PATCH 151/258] Fix humidifier platform for Comelit (#141854) * Fix humidifier platform for Comelit * apply review comment --- homeassistant/components/comelit/humidifier.py | 6 +++++- homeassistant/components/comelit/strings.json | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/comelit/humidifier.py b/homeassistant/components/comelit/humidifier.py index ad8f49ed5e2..d7b20f731a9 100644 --- a/homeassistant/components/comelit/humidifier.py +++ b/homeassistant/components/comelit/humidifier.py @@ -162,7 +162,7 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" - if self.mode == HumidifierComelitMode.OFF: + if not self._attr_is_on: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="humidity_while_off", @@ -190,9 +190,13 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier await self.coordinator.api.set_humidity_status( self._device.index, self._set_command ) + self._attr_is_on = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off.""" await self.coordinator.api.set_humidity_status( self._device.index, HumidifierComelitCommand.OFF ) + self._attr_is_on = False + self.async_write_ha_state() diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 496d62655a9..f9771e58a40 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -52,7 +52,9 @@ "rest": "Rest", "sabotated": "Sabotated" } - }, + } + }, + "humidifier": { "humidifier": { "name": "Humidifier" }, From f8d284ec4ba8958aa4061090de28b643c5c45bbe Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 4 Apr 2025 08:52:18 +0100 Subject: [PATCH 152/258] Bump evohome-async to 1.0.5 (#141871) bump client to 1.0.5 --- homeassistant/components/evohome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 44e4cdb1128..21c8874135a 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -6,5 +6,5 @@ "iot_class": "cloud_polling", "loggers": ["evohome", "evohomeasync", "evohomeasync2"], "quality_scale": "legacy", - "requirements": ["evohome-async==1.0.4"] + "requirements": ["evohome-async==1.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 336d8c2110b..c07a8b3a27c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -901,7 +901,7 @@ eufylife-ble-client==0.1.8 # evdev==1.6.1 # homeassistant.components.evohome -evohome-async==1.0.4 +evohome-async==1.0.5 # homeassistant.components.bryant_evolution evolutionhttp==0.0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e27279ea9db..b2fe9deb0c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -768,7 +768,7 @@ eternalegypt==0.0.16 eufylife-ble-client==0.1.8 # homeassistant.components.evohome -evohome-async==1.0.4 +evohome-async==1.0.5 # homeassistant.components.bryant_evolution evolutionhttp==0.0.18 From 109d20978fdc7e61e2d52d154f004b66ce91dbb3 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 2 Apr 2025 17:33:36 +0200 Subject: [PATCH 153/258] Replace "to log into" with "to log in to" in `incomfort` (#142060) * Replace "to log into" with "to log in to" in `incomfort` Also fix one missing sentence-casing of "gateway". * Replace duplicate "data_description" strings with references --- homeassistant/components/incomfort/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/incomfort/strings.json b/homeassistant/components/incomfort/strings.json index 73ba88078a8..4e1168686df 100644 --- a/homeassistant/components/incomfort/strings.json +++ b/homeassistant/components/incomfort/strings.json @@ -10,8 +10,8 @@ }, "data_description": { "host": "Hostname or IP-address of the Intergas gateway.", - "username": "The username to log into the gateway. This is `admin` in most cases.", - "password": "The password to log into the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices." + "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." } }, "dhcp_auth": { @@ -22,8 +22,8 @@ "password": "[%key:common::config_flow::data::password%]" }, "data_description": { - "username": "The username to log into the gateway. This is `admin` in most cases.", - "password": "The password to log into the gateway, is printed at the bottom of the Gateway or is `intergas` for some older devices." + "username": "[%key:component::incomfort::config::step::user::data_description::username%]", + "password": "[%key:component::incomfort::config::step::user::data_description::password%]" } }, "dhcp_confirm": { From 854cae7f12997fe9aa596976b32bbd1d1accd93b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Apr 2025 22:39:35 +0200 Subject: [PATCH 154/258] Avoid unnecessary reload in apple_tv reauth flow (#142079) --- homeassistant/components/apple_tv/config_flow.py | 5 ++++- tests/components/apple_tv/test_config_flow.py | 13 +++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 76c4681a30d..b026da33231 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -20,6 +20,7 @@ import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import ( SOURCE_IGNORE, + SOURCE_REAUTH, SOURCE_ZEROCONF, ConfigEntry, ConfigFlow, @@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN): CONF_IDENTIFIERS: list(combined_identifiers), }, ) - if entry.source != SOURCE_IGNORE: + # Don't reload ignored entries or in the middle of reauth, + # e.g. if the user is entering a new PIN + if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH: self.hass.config_entries.async_schedule_reload(entry.entry_id) if not allow_exist: raise DeviceAlreadyConfigured diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index a13eb3c605b..c9d698e068b 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -59,12 +59,12 @@ def use_mocked_zeroconf(mock_async_zeroconf: MagicMock) -> None: @pytest.fixture(autouse=True) -def mock_setup_entry() -> Generator[None]: +def mock_setup_entry() -> Generator[Mock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.apple_tv.async_setup_entry", return_value=True - ): - yield + ) as setup_entry: + yield setup_entry # User Flows @@ -1183,7 +1183,9 @@ async def test_zeroconf_mismatch(hass: HomeAssistant, mock_scan: AsyncMock) -> N @pytest.mark.usefixtures("mrp_device", "pairing") -async def test_reconfigure_update_credentials(hass: HomeAssistant) -> None: +async def test_reconfigure_update_credentials( + hass: HomeAssistant, mock_setup_entry: Mock +) -> None: """Test that reconfigure flow updates config entry.""" config_entry = MockConfigEntry( domain="apple_tv", unique_id="mrpid", data={"identifiers": ["mrpid"]} @@ -1215,6 +1217,9 @@ async def test_reconfigure_update_credentials(hass: HomeAssistant) -> None: "identifiers": ["mrpid"], } + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + # Options From 04be575139c3316d22987fb3d938ed66125973a8 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 3 Apr 2025 13:13:52 +0200 Subject: [PATCH 155/258] Add translation for hassio update entity name (#142090) --- homeassistant/components/hassio/strings.json | 5 +++++ homeassistant/components/hassio/update.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index a543dbc7f89..71d7a6669f3 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -265,6 +265,11 @@ "version_latest": { "name": "Newest version" } + }, + "update": { + "update": { + "name": "[%key:component::update::title%]" + } } }, "services": { diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index 4ea703e87c3..263cf2dfe13 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -39,7 +39,7 @@ from .entity import ( from .update_helper import update_addon, update_core ENTITY_DESCRIPTION = UpdateEntityDescription( - name="Update", + translation_key="update", key=ATTR_VERSION_LATEST, ) From 490e3201b9deacfecaf405486f4f18eaf2b67647 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Thu, 3 Apr 2025 09:46:09 +0200 Subject: [PATCH 156/258] Bump pyenphase to 1.25.5 (#142107) --- 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 e51a7427504..88183fe4cfd 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.25.1"], + "requirements": ["pyenphase==1.25.5"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index c07a8b3a27c..b6db3c416a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1948,7 +1948,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.25.1 +pyenphase==1.25.5 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2fe9deb0c2..f83892cd83d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1590,7 +1590,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.25.1 +pyenphase==1.25.5 # homeassistant.components.everlights pyeverlights==0.1.0 From 7546b5d269e89db415de31ece2c1e1efb2127209 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:09:13 -0400 Subject: [PATCH 157/258] Hide broken ZBT-1 config entries on the hardware page (#142110) * Hide bad ZBT-1 config entries on the hardware page * Set up the bad config entry in the unit test * Roll into a list comprehension * Remove constant changes * Fix condition in unit test --- .../homeassistant_sky_connect/hardware.py | 8 +++++++- .../homeassistant_sky_connect/test_hardware.py | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index 2872077111a..9bfa5d16655 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -5,17 +5,21 @@ from __future__ import annotations from homeassistant.components.hardware.models import HardwareInfo, USBInfo from homeassistant.core import HomeAssistant, callback +from .config_flow import HomeAssistantSkyConnectConfigFlow from .const import DOMAIN from .util import get_hardware_variant DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/" +EXPECTED_ENTRY_VERSION = ( + HomeAssistantSkyConnectConfigFlow.VERSION, + HomeAssistantSkyConnectConfigFlow.MINOR_VERSION, +) @callback def async_info(hass: HomeAssistant) -> list[HardwareInfo]: """Return board info.""" entries = hass.config_entries.async_entries(DOMAIN) - return [ HardwareInfo( board=None, @@ -31,4 +35,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: url=DOCUMENTATION_URL, ) for entry in entries + # Ignore unmigrated config entries in the hardware page + if (entry.version, entry.minor_version) == EXPECTED_ENTRY_VERSION ] diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index f39e648b0f2..e59a1e7df06 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -28,6 +28,10 @@ CONFIG_ENTRY_DATA_2 = { "firmware": "ezsp", } +CONFIG_ENTRY_DATA_BAD = { + "device": "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_a87b7d75b18beb119fe564a0f320645d-if00-port0", +} + async def test_hardware_info( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, addon_store_info @@ -59,9 +63,20 @@ async def test_hardware_info( minor_version=2, ) config_entry_2.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry_2.entry_id) + config_entry_bad = MockConfigEntry( + data=CONFIG_ENTRY_DATA_BAD, + domain=DOMAIN, + options={}, + title="Home Assistant Connect ZBT-1", + unique_id="unique_3", + version=1, + minor_version=2, + ) + config_entry_bad.add_to_hass(hass) + assert not await hass.config_entries.async_setup(config_entry_bad.entry_id) + client = await hass_ws_client(hass) await client.send_json({"id": 1, "type": "hardware/info"}) @@ -97,5 +112,6 @@ async def test_hardware_info( "name": "Home Assistant Connect ZBT-1", "url": "https://skyconnect.home-assistant.io/documentation/", }, + # Bad entry is skipped ] } From 281beecb05b13f97177addba599116287f093672 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 3 Apr 2025 09:43:00 +0200 Subject: [PATCH 158/258] Bump pysmhi to 1.0.1 (#142111) --- homeassistant/components/smhi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index fc3af634764..89443fc7e27 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/smhi", "iot_class": "cloud_polling", "loggers": ["pysmhi"], - "requirements": ["pysmhi==1.0.0"] + "requirements": ["pysmhi==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b6db3c416a2..26be389e357 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2325,7 +2325,7 @@ pysmartthings==3.0.1 pysmarty2==0.10.2 # homeassistant.components.smhi -pysmhi==1.0.0 +pysmhi==1.0.1 # homeassistant.components.edl21 pysml==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f83892cd83d..72f73726d7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1895,7 +1895,7 @@ pysmartthings==3.0.1 pysmarty2==0.10.2 # homeassistant.components.smhi -pysmhi==1.0.0 +pysmhi==1.0.1 # homeassistant.components.edl21 pysml==0.0.12 From fe65579df8f03c4f7f239453dd13f3285c02b118 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Apr 2025 21:38:50 -1000 Subject: [PATCH 159/258] Avoid logging a warning when replacing an ignored config entry (#142114) Replacing an ignored config entry with one from the user flow should not generate a warning. We should only warn if we are replacing a usable config entry. Followup to adjust the warning added in #130567 cc @epenet --- homeassistant/config_entries.py | 6 +++++- tests/test_config_entries.py | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d3e681ecca1..8c7ae5fe356 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1626,7 +1626,11 @@ class ConfigEntriesFlowManager( result["handler"], flow.unique_id ) - if existing_entry is not None and flow.handler != "mobile_app": + if ( + existing_entry is not None + and flow.handler != "mobile_app" + and existing_entry.source != SOURCE_IGNORE + ): # This causes the old entry to be removed and replaced, when the flow # should instead be aborted. # In case of manual flows, integrations should implement options, reauth, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index e3b80ecc03f..b38fed60e85 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -8883,15 +8883,17 @@ async def test_add_description_placeholder_automatically_not_overwrites( @pytest.mark.parametrize( - ("domain", "expected_log"), + ("domain", "source", "expected_log"), [ - ("some_integration", True), - ("mobile_app", False), + ("some_integration", config_entries.SOURCE_USER, True), + ("some_integration", config_entries.SOURCE_IGNORE, False), + ("mobile_app", config_entries.SOURCE_USER, False), ], ) async def test_create_entry_existing_unique_id( hass: HomeAssistant, domain: str, + source: str, expected_log: bool, caplog: pytest.LogCaptureFixture, ) -> None: @@ -8902,6 +8904,7 @@ async def test_create_entry_existing_unique_id( entry_id="01J915Q6T9F6G5V0QJX6HBC94T", data={"host": "any", "port": 123}, unique_id="mock-unique-id", + source=source, ) entry.add_to_hass(hass) From e5c00eceae8bf0c17dbbf49be9451143ba3f343f Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 4 Apr 2025 16:51:09 +1000 Subject: [PATCH 160/258] Slow down polling in Tesla Fleet (#142130) * Slow down polling * Fix tests --- .../components/tesla_fleet/coordinator.py | 2 +- tests/components/tesla_fleet/test_init.py | 88 ++++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py index 50a69258a31..20d2d70b5dc 100644 --- a/homeassistant/components/tesla_fleet/coordinator.py +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -27,7 +27,7 @@ if TYPE_CHECKING: from .const import ENERGY_HISTORY_FIELDS, LOGGER, TeslaFleetState -VEHICLE_INTERVAL_SECONDS = 300 +VEHICLE_INTERVAL_SECONDS = 600 VEHICLE_INTERVAL = timedelta(seconds=VEHICLE_INTERVAL_SECONDS) VEHICLE_WAIT = timedelta(minutes=15) diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index ff103ce03c2..7bd90a3568c 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -1,6 +1,7 @@ """Test the Tesla Fleet init.""" from copy import deepcopy +from datetime import timedelta from unittest.mock import AsyncMock, patch from aiohttp import RequestInfo @@ -231,57 +232,58 @@ async def test_vehicle_sleep( freezer: FrozenDateTimeFactory, ) -> None: """Test coordinator refresh with an error.""" - await setup_platform(hass, normal_config_entry) - assert mock_vehicle_data.call_count == 1 - freezer.tick(VEHICLE_WAIT + VEHICLE_INTERVAL) - async_fire_time_changed(hass) - # Let vehicle sleep, no updates for 15 minutes - await hass.async_block_till_done() - assert mock_vehicle_data.call_count == 2 + TEST_INTERVAL = timedelta(seconds=120) - freezer.tick(VEHICLE_INTERVAL) - async_fire_time_changed(hass) - # No polling, call_count should not increase - await hass.async_block_till_done() - assert mock_vehicle_data.call_count == 2 + with patch( + "homeassistant.components.tesla_fleet.coordinator.VEHICLE_INTERVAL", + TEST_INTERVAL, + ): + await setup_platform(hass, normal_config_entry) + assert mock_vehicle_data.call_count == 1 - freezer.tick(VEHICLE_INTERVAL) - async_fire_time_changed(hass) - # No polling, call_count should not increase - await hass.async_block_till_done() - assert mock_vehicle_data.call_count == 2 + freezer.tick(VEHICLE_WAIT + TEST_INTERVAL) + async_fire_time_changed(hass) + # Let vehicle sleep, no updates for 15 minutes + await hass.async_block_till_done() + assert mock_vehicle_data.call_count == 2 - freezer.tick(VEHICLE_WAIT) - async_fire_time_changed(hass) - # Vehicle didn't sleep, go back to normal - await hass.async_block_till_done() - assert mock_vehicle_data.call_count == 3 + freezer.tick(TEST_INTERVAL) + async_fire_time_changed(hass) + # No polling, call_count should not increase + await hass.async_block_till_done() + assert mock_vehicle_data.call_count == 2 - freezer.tick(VEHICLE_INTERVAL) - async_fire_time_changed(hass) - # Regular polling - await hass.async_block_till_done() - assert mock_vehicle_data.call_count == 4 + freezer.tick(VEHICLE_WAIT) + async_fire_time_changed(hass) + # Vehicle didn't sleep, go back to normal + await hass.async_block_till_done() + assert mock_vehicle_data.call_count == 3 - mock_vehicle_data.return_value = VEHICLE_DATA_ALT - freezer.tick(VEHICLE_INTERVAL) - async_fire_time_changed(hass) - # Vehicle active - await hass.async_block_till_done() - assert mock_vehicle_data.call_count == 5 + freezer.tick(TEST_INTERVAL) + async_fire_time_changed(hass) + # Regular polling + await hass.async_block_till_done() + assert mock_vehicle_data.call_count == 4 - freezer.tick(VEHICLE_WAIT) - async_fire_time_changed(hass) - # Dont let sleep when active - await hass.async_block_till_done() - assert mock_vehicle_data.call_count == 6 + mock_vehicle_data.return_value = VEHICLE_DATA_ALT + freezer.tick(TEST_INTERVAL) + async_fire_time_changed(hass) + # Vehicle active + await hass.async_block_till_done() + assert mock_vehicle_data.call_count == 5 - freezer.tick(VEHICLE_WAIT) - async_fire_time_changed(hass) - # Dont let sleep when active - await hass.async_block_till_done() - assert mock_vehicle_data.call_count == 7 + freezer.tick(TEST_INTERVAL) + async_fire_time_changed(hass) + # Dont let sleep when active + await hass.async_block_till_done() + assert mock_vehicle_data.call_count == 6 + + freezer.tick(TEST_INTERVAL) + async_fire_time_changed(hass) + # Dont let sleep when active + await hass.async_block_till_done() + assert mock_vehicle_data.call_count == 7 # Test Energy Live Coordinator From 6410b90d82c94597c8255f56e98dcb7f6672afdd Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 3 Apr 2025 16:12:41 +1000 Subject: [PATCH 161/258] Bump tesla-fleet-api to v1.0.17 (#142131) 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 56dc49ad111..53c8e7d554c 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.16"] + "requirements": ["tesla-fleet-api==1.0.17"] } diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index cae5a8f3c01..a8f1fd0daec 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.16", "teslemetry-stream==0.6.12"] + "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.6.12"] } diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index 3f96bb226ab..3f71bcb95e3 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.16"] + "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.0.17"] } diff --git a/requirements_all.txt b/requirements_all.txt index 26be389e357..f3d2be7fd75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2878,7 +2878,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==1.0.16 +tesla-fleet-api==1.0.17 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72f73726d7f..a0fdd2993a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2316,7 +2316,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==1.0.16 +tesla-fleet-api==1.0.17 # homeassistant.components.powerwall tesla-powerwall==0.5.2 From 4643fc2c1415307058ff5d6b6b0ffc2b7a8d8480 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 3 Apr 2025 20:30:34 +0200 Subject: [PATCH 162/258] Tado bump to 0.18.11 (#142175) * Bump to version 0.18.11 * Adding hassfest files --- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 75ddbacc585..eba13d469f3 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.9"] + "requirements": ["python-tado==0.18.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index f3d2be7fd75..fd8b989b0f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2482,7 +2482,7 @@ python-snoo==0.6.5 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.18.9 +python-tado==0.18.11 # homeassistant.components.technove python-technove==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0fdd2993a5..2af6aed7003 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2013,7 +2013,7 @@ python-snoo==0.6.5 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.18.9 +python-tado==0.18.11 # homeassistant.components.technove python-technove==2.0.0 From 2a4996055a97e51dcd4a582893b51e8581d24314 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 3 Apr 2025 20:12:24 +0200 Subject: [PATCH 163/258] Add preset mode to SmartThings climate (#142180) * Add preset mode to SmartThings climate * Add preset mode to SmartThings climate --- homeassistant/components/smartthings/climate.py | 13 ++++++++++++- .../fixtures/device_status/da_ac_rac_000001.json | 2 +- .../smartthings/snapshots/test_climate.ambr | 2 +- .../smartthings/snapshots/test_diagnostics.ambr | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 49499732c24..f2f9479584c 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -333,7 +333,6 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): """Define a SmartThings Air Conditioner.""" _attr_name = None - _attr_preset_mode = None def __init__(self, client: SmartThings, device: FullDevice) -> None: """Init the class.""" @@ -545,6 +544,18 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): SWING_OFF, ) + @property + def preset_mode(self) -> str | None: + """Return the preset mode.""" + if self.supports_capability(Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE): + mode = self.get_attribute_value( + Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE, + Attribute.AC_OPTIONAL_MODE, + ) + if mode == WINDFREE: + return WINDFREE + return None + def _determine_preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" if self.supports_capability(Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE): diff --git a/tests/components/smartthings/fixtures/device_status/da_ac_rac_000001.json b/tests/components/smartthings/fixtures/device_status/da_ac_rac_000001.json index c80fcf9c298..f6cdd661a99 100644 --- a/tests/components/smartthings/fixtures/device_status/da_ac_rac_000001.json +++ b/tests/components/smartthings/fixtures/device_status/da_ac_rac_000001.json @@ -473,7 +473,7 @@ "timestamp": "2024-09-10T10:26:28.781Z" }, "acOptionalMode": { - "value": "off", + "value": "windFree", "timestamp": "2025-02-09T09:14:39.642Z" } }, diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index 19cfe971d7f..633b02568fc 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -211,7 +211,7 @@ ]), 'max_temp': 35, 'min_temp': 7, - 'preset_mode': None, + 'preset_mode': 'windFree', 'preset_modes': list([ 'windFree', ]), diff --git a/tests/components/smartthings/snapshots/test_diagnostics.ambr b/tests/components/smartthings/snapshots/test_diagnostics.ambr index b9847bf9746..dc7f699de27 100644 --- a/tests/components/smartthings/snapshots/test_diagnostics.ambr +++ b/tests/components/smartthings/snapshots/test_diagnostics.ambr @@ -1065,7 +1065,7 @@ 'custom.airConditionerOptionalMode': dict({ 'acOptionalMode': dict({ 'timestamp': '2025-02-09T09:14:39.642Z', - 'value': 'off', + 'value': 'windFree', }), 'supportedAcOptionalMode': dict({ 'timestamp': '2024-09-10T10:26:28.781Z', From c43a4682b9e22ae5992c91419944e7e94cd38a23 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 3 Apr 2025 22:26:56 +0200 Subject: [PATCH 164/258] Do not create a HA mediaplayer for the builtin Music Assistant player (#142192) Do not create a HA mediaplayer for the builtin Music player --- homeassistant/components/music_assistant/media_player.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index 7d26f5b3a0c..005e5006b52 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -151,6 +151,8 @@ async def async_setup_entry( assert event.object_id is not None if event.object_id in added_ids: return + if not player.expose_to_ha: + return added_ids.add(event.object_id) async_add_entities([MusicAssistantPlayer(mass, event.object_id)]) @@ -159,6 +161,8 @@ async def async_setup_entry( mass_players = [] # add all current players for player in mass.players: + if not player.expose_to_ha: + continue added_ids.add(player.player_id) mass_players.append(MusicAssistantPlayer(mass, player.player_id)) From 9db60c830cdcfbdf206d979bbe2b600212bd2236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Fri, 4 Apr 2025 09:56:40 +0200 Subject: [PATCH 165/258] Do not fetch disconnected Home Connect appliances (#142200) * Do not fetch disconnected Home Connect appliances * Apply suggestions Co-authored-by: Martin Hjelmare * Update docstring --------- Co-authored-by: Martin Hjelmare --- .../components/home_connect/coordinator.py | 32 ++++++--- .../home_connect/test_coordinator.py | 71 +++++++++++++++++-- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/home_connect/coordinator.py b/homeassistant/components/home_connect/coordinator.py index 079db6b148e..edaf5ea7332 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -73,6 +73,19 @@ class HomeConnectApplianceData: self.settings.update(other.settings) self.status.update(other.status) + @classmethod + def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData: + """Return empty data.""" + return cls( + commands=set(), + events={}, + info=appliance, + options={}, + programs=[], + settings={}, + status={}, + ) + class HomeConnectCoordinator( DataUpdateCoordinator[dict[str, HomeConnectApplianceData]] @@ -358,15 +371,7 @@ class HomeConnectCoordinator( model=appliance.vib, ) if appliance.ha_id not in self.data: - self.data[appliance.ha_id] = HomeConnectApplianceData( - commands=set(), - events={}, - info=appliance, - options={}, - programs=[], - settings={}, - status={}, - ) + self.data[appliance.ha_id] = HomeConnectApplianceData.empty(appliance) else: self.data[appliance.ha_id].info.connected = appliance.connected old_appliances.remove(appliance.ha_id) @@ -402,6 +407,15 @@ class HomeConnectCoordinator( name=appliance.name, model=appliance.vib, ) + if not appliance.connected: + _LOGGER.debug( + "Appliance %s is not connected, skipping data fetch", + appliance.ha_id, + ) + if appliance_data_to_update: + appliance_data_to_update.info.connected = False + return appliance_data_to_update + return HomeConnectApplianceData.empty(appliance) try: settings = { setting.key: setting diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index 050758a6568..68f8299e613 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -54,6 +54,14 @@ from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed +INITIAL_FETCH_CLIENT_METHODS = [ + "get_settings", + "get_status", + "get_all_programs", + "get_available_commands", + "get_available_program", +] + @pytest.fixture def platforms() -> list[str]: @@ -214,15 +222,32 @@ async def test_coordinator_failure_refresh_and_stream( assert state.state != STATE_UNAVAILABLE +@pytest.mark.parametrize( + "appliance", + ["Dishwasher"], + indirect=True, +) +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: + """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 + + for method in INITIAL_FETCH_CLIENT_METHODS: + assert getattr(client, method).call_count == 0 + + @pytest.mark.parametrize( "mock_method", - [ - "get_settings", - "get_status", - "get_all_programs", - "get_available_commands", - "get_available_program", - ], + INITIAL_FETCH_CLIENT_METHODS, ) async def test_coordinator_update_failing( mock_method: str, @@ -551,3 +576,35 @@ async def test_devices_updated_on_refresh( assert not device_registry.async_get_device({(DOMAIN, appliances[0].ha_id)}) for appliance in appliances[2:3]: assert device_registry.async_get_device({(DOMAIN, appliance.ha_id)}) + + +@pytest.mark.parametrize("appliance", ["Washer"], indirect=True) +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: + """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 + + appliance.connected = False + await client.add_events( + [ + EventMessage( + appliance.ha_id, + EventType.PAIRED, + data=ArrayOfEvents([]), + ) + ] + ) + await hass.async_block_till_done() + + client.get_specific_appliance.assert_awaited_once_with(appliance.ha_id) + for method in INITIAL_FETCH_CLIENT_METHODS: + assert getattr(client, method).call_count == 0 From 86eee4f04136ff60419146b9c51ebff8ece2ab73 Mon Sep 17 00:00:00 2001 From: rappenze Date: Thu, 3 Apr 2025 18:36:44 +0200 Subject: [PATCH 166/258] Fix fibaro setup (#142201) --- homeassistant/components/fibaro/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 33b2598a636..a4f59d8ab76 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -301,6 +301,7 @@ class FibaroController: device.ha_id = ( f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}" ) + platform = None if device.enabled and (not device.is_plugin or self._import_plugins): platform = self._map_device_to_platform(device) if platform is None: From e9abdab1f56d9f2f0ca6f74754f08722155f8c90 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 4 Apr 2025 21:19:15 +0200 Subject: [PATCH 167/258] Fix circular mean by always storing and using the weighted one (#142208) * Fix circular mean by always storing and using the weighted one * fix * Fix test --- .../components/recorder/statistics.py | 86 +++++++++++-------- homeassistant/components/sensor/recorder.py | 4 +- tests/components/sensor/test_recorder.py | 46 +++++----- 3 files changed, 76 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2507a66899e..80c0028ef7a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -139,14 +139,13 @@ def query_circular_mean(table: type[StatisticsBase]) -> tuple[Label, Label]: # in Python. # https://en.wikipedia.org/wiki/Circular_mean radians = func.radians(table.mean) + weighted_sum_sin = func.sum(func.sin(radians) * table.mean_weight) + weighted_sum_cos = func.sum(func.cos(radians) * table.mean_weight) weight = func.sqrt( - func.power(func.sum(func.sin(radians) * table.mean_weight), 2) - + func.power(func.sum(func.cos(radians) * table.mean_weight), 2) + func.power(weighted_sum_sin, 2) + func.power(weighted_sum_cos, 2) ) return ( - func.degrees( - func.atan2(func.sum(func.sin(radians)), func.sum(func.cos(radians))) - ).label("mean"), + func.degrees(func.atan2(weighted_sum_sin, weighted_sum_cos)).label("mean"), weight.label("mean_weight"), ) @@ -240,18 +239,20 @@ DEG_TO_RAD = math.pi / 180 RAD_TO_DEG = 180 / math.pi -def weighted_circular_mean(values: Iterable[tuple[float, float]]) -> float: - """Return the weighted circular mean of the values.""" - sin_sum = sum(math.sin(x * DEG_TO_RAD) * weight for x, weight in values) - cos_sum = sum(math.cos(x * DEG_TO_RAD) * weight for x, weight in values) - return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360 +def weighted_circular_mean( + values: Iterable[tuple[float, float]], +) -> tuple[float, float]: + """Return the weighted circular mean and the weight of the values.""" + weighted_sin_sum, weighted_cos_sum = 0.0, 0.0 + for x, weight in values: + rad_x = x * DEG_TO_RAD + weighted_sin_sum += math.sin(rad_x) * weight + weighted_cos_sum += math.cos(rad_x) * weight - -def circular_mean(values: list[float]) -> float: - """Return the circular mean of the values.""" - sin_sum = sum(math.sin(x * DEG_TO_RAD) for x in values) - cos_sum = sum(math.cos(x * DEG_TO_RAD) for x in values) - return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360 + return ( + (RAD_TO_DEG * math.atan2(weighted_sin_sum, weighted_cos_sum)) % 360, + math.sqrt(weighted_sin_sum**2 + weighted_cos_sum**2), + ) _LOGGER = logging.getLogger(__name__) @@ -300,6 +301,7 @@ class StatisticsRow(BaseStatisticsRow, total=False): min: float | None max: float | None mean: float | None + mean_weight: float | None change: float | None @@ -1023,7 +1025,7 @@ def _reduce_statistics( _want_sum = "sum" in types for statistic_id, stat_list in stats.items(): max_values: list[float] = [] - mean_values: list[float] = [] + mean_values: list[tuple[float, float]] = [] min_values: list[float] = [] prev_stat: StatisticsRow = stat_list[0] fake_entry: StatisticsRow = {"start": stat_list[-1]["start"] + period_seconds} @@ -1039,12 +1041,15 @@ def _reduce_statistics( } if _want_mean: row["mean"] = None + row["mean_weight"] = None if mean_values: match metadata[statistic_id][1]["mean_type"]: case StatisticMeanType.ARITHMETIC: - row["mean"] = mean(mean_values) + row["mean"] = mean([x[0] for x in mean_values]) case StatisticMeanType.CIRCULAR: - row["mean"] = circular_mean(mean_values) + row["mean"], row["mean_weight"] = ( + weighted_circular_mean(mean_values) + ) mean_values.clear() if _want_min: row["min"] = min(min_values) if min_values else None @@ -1063,7 +1068,8 @@ def _reduce_statistics( max_values.append(_max) if _want_mean: if (_mean := statistic.get("mean")) is not None: - mean_values.append(_mean) + _mean_weight = statistic.get("mean_weight") or 0.0 + mean_values.append((_mean, _mean_weight)) if _want_min and (_min := statistic.get("min")) is not None: min_values.append(_min) prev_stat = statistic @@ -1385,7 +1391,7 @@ def _get_max_mean_min_statistic( match metadata[1]["mean_type"]: case StatisticMeanType.CIRCULAR: if circular_means := max_mean_min["circular_means"]: - mean_value = weighted_circular_mean(circular_means) + mean_value = weighted_circular_mean(circular_means)[0] case StatisticMeanType.ARITHMETIC: if (mean_value := max_mean_min.get("mean_acc")) is not None and ( duration := max_mean_min.get("duration") @@ -1739,12 +1745,12 @@ def statistic_during_period( _type_column_mapping = { - "last_reset": "last_reset_ts", - "max": "max", - "mean": "mean", - "min": "min", - "state": "state", - "sum": "sum", + "last_reset": ("last_reset_ts",), + "max": ("max",), + "mean": ("mean", "mean_weight"), + "min": ("min",), + "state": ("state",), + "sum": ("sum",), } @@ -1756,12 +1762,13 @@ def _generate_select_columns_for_types_stmt( track_on: list[str | None] = [ table.__tablename__, # type: ignore[attr-defined] ] - for key, column in _type_column_mapping.items(): - if key in types: - columns = columns.add_columns(getattr(table, column)) - track_on.append(column) - else: - track_on.append(None) + for key, type_columns in _type_column_mapping.items(): + for column in type_columns: + if key in types: + columns = columns.add_columns(getattr(table, column)) + track_on.append(column) + else: + track_on.append(None) return lambda_stmt(lambda: columns, track_on=track_on) @@ -1944,6 +1951,12 @@ def _statistics_during_period_with_session( hass, session, start_time, units, _types, table, metadata, result ) + # filter out mean_weight as it is only needed to reduce statistics + # and not needed in the result + for stats_rows in result.values(): + for row in stats_rows: + row.pop("mean_weight", None) + # Return statistics combined with metadata return result @@ -2391,7 +2404,12 @@ def _sorted_statistics_to_dict( field_map["last_reset"] = field_map.pop("last_reset_ts") sum_idx = field_map["sum"] if "sum" in types else None sum_only = len(types) == 1 and sum_idx is not None - row_mapping = tuple((key, field_map[key]) for key in types if key in field_map) + row_mapping = tuple( + (column, field_map[column]) + for key in types + for column in ({key, *_type_column_mapping.get(key, ())}) + if column in field_map + ) # Append all statistic entries, and optionally do unit conversion table_duration_seconds = table.duration.total_seconds() for meta_id, db_rows in stats_by_meta_id.items(): diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index cb80fa7d2ce..c321caa616d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -160,7 +160,7 @@ def _time_weighted_arithmetic_mean( def _time_weighted_circular_mean( fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime -) -> float: +) -> tuple[float, float]: """Calculate a time weighted circular mean. The circular mean is calculated by weighting the states by duration in seconds between @@ -623,7 +623,7 @@ def compile_statistics( # noqa: C901 valid_float_states, start, end ) case StatisticMeanType.CIRCULAR: - stat["mean"] = _time_weighted_circular_mean( + stat["mean"], stat["mean_weight"] = _time_weighted_circular_mean( valid_float_states, start, end ) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 962c0a0ef8f..43f185f939a 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -4508,23 +4508,19 @@ async def test_compile_statistics_hourly_daily_monthly_summary( duration += dur return total / duration - def _time_weighted_circular_mean(values: list[tuple[float, int]]): + def _weighted_circular_mean( + values: Iterable[tuple[float, float]], + ) -> tuple[float, float]: sin_sum = 0 cos_sum = 0 - for x, dur in values: - sin_sum += math.sin(x * DEG_TO_RAD) * dur - cos_sum += math.cos(x * DEG_TO_RAD) * dur + for x, weight in values: + sin_sum += math.sin(x * DEG_TO_RAD) * weight + cos_sum += math.cos(x * DEG_TO_RAD) * weight - return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360 - - def _circular_mean(values: list[float]) -> float: - sin_sum = 0 - cos_sum = 0 - for x in values: - sin_sum += math.sin(x * DEG_TO_RAD) - cos_sum += math.cos(x * DEG_TO_RAD) - - return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360 + return ( + (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360, + math.sqrt(sin_sum**2 + cos_sum**2), + ) def _min(seq, last_state): if last_state is None: @@ -4631,7 +4627,7 @@ async def test_compile_statistics_hourly_daily_monthly_summary( values = [(seq, durations[j]) for j, seq in enumerate(seq)] if (state := last_states["sensor.test5"]) is not None: values.append((state, 5)) - expected_means["sensor.test5"].append(_time_weighted_circular_mean(values)) + expected_means["sensor.test5"].append(_weighted_circular_mean(values)) last_states["sensor.test5"] = seq[-1] start += timedelta(minutes=5) @@ -4733,15 +4729,17 @@ async def test_compile_statistics_hourly_daily_monthly_summary( start = zero end = zero + timedelta(minutes=5) for i in range(24): - for entity_id in ( - "sensor.test1", - "sensor.test2", - "sensor.test3", - "sensor.test4", - "sensor.test5", + for entity_id, mean_extractor in ( + ("sensor.test1", lambda x: x), + ("sensor.test2", lambda x: x), + ("sensor.test3", lambda x: x), + ("sensor.test4", lambda x: x), + ("sensor.test5", lambda x: x[0]), ): expected_average = ( - expected_means[entity_id][i] if entity_id in expected_means else None + mean_extractor(expected_means[entity_id][i]) + if entity_id in expected_means + else None ) expected_minimum = ( expected_minima[entity_id][i] if entity_id in expected_minima else None @@ -4772,7 +4770,7 @@ async def test_compile_statistics_hourly_daily_monthly_summary( assert stats == expected_stats def verify_stats( - period: Literal["5minute", "day", "hour", "week", "month"], + period: Literal["hour", "day", "week", "month"], start: datetime, next_datetime: Callable[[datetime], datetime], ) -> None: @@ -4791,7 +4789,7 @@ async def test_compile_statistics_hourly_daily_monthly_summary( ("sensor.test2", mean), ("sensor.test3", mean), ("sensor.test4", mean), - ("sensor.test5", _circular_mean), + ("sensor.test5", lambda x: _weighted_circular_mean(x)[0]), ): expected_average = ( mean_fn(expected_means[entity_id][i * 12 : (i + 1) * 12]) From 7bd517e6ff40b21549e202e4c399ef1750eed5e2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 4 Apr 2025 17:52:06 +0200 Subject: [PATCH 168/258] Bump pySmartThings to 3.0.2 (#142257) Co-authored-by: Robert Resch --- 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 2af3e5c193b..dda7ef53cf5 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.0.1"] + "requirements": ["pysmartthings==3.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index fd8b989b0f6..62c573f2275 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2319,7 +2319,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.0.1 +pysmartthings==3.0.2 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2af6aed7003..6bde6979cbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1889,7 +1889,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.0.1 +pysmartthings==3.0.2 # homeassistant.components.smarty pysmarty2==0.10.2 From f46e6597405bd730aea94a06ea1c9980c026c664 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Apr 2025 16:38:56 +0200 Subject: [PATCH 169/258] Update frontend to 20250404.0 (#142274) --- 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 4cab8375d1b..140d90c5dbe 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==20250401.0"] + "requirements": ["home-assistant-frontend==20250404.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 658fdc55e21..b6dec576591 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.37.0 hass-nabucasa==0.94.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250401.0 +home-assistant-frontend==20250404.0 home-assistant-intents==2025.3.28 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 62c573f2275..624f52340ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250401.0 +home-assistant-frontend==20250404.0 # homeassistant.components.conversation home-assistant-intents==2025.3.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bde6979cbc..30641440d8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250401.0 +home-assistant-frontend==20250404.0 # homeassistant.components.conversation home-assistant-intents==2025.3.28 From af708b78e08888c9017fc80b3f285425eb419045 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 4 Apr 2025 21:18:09 +0200 Subject: [PATCH 170/258] Bump forecast-solar lib to v4.1.0 (#142280) Co-authored-by: Jan-Philipp Benecke --- 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 1eb9c98701d..769bda56adc 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.0.0"] + "requirements": ["forecast-solar==4.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 624f52340ac..b6ee4cc0c00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -954,7 +954,7 @@ fnv-hash-fast==1.4.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==4.0.0 +forecast-solar==4.1.0 # homeassistant.components.fortios fortiosapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30641440d8b..f86b6bba123 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -814,7 +814,7 @@ fnv-hash-fast==1.4.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==4.0.0 +forecast-solar==4.1.0 # homeassistant.components.freebox freebox-api==1.2.2 From 4f799069ea7f6bcffbdc1ce19a886d1f46239962 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Apr 2025 19:24:45 +0000 Subject: [PATCH 171/258] Bump version to 2025.4.1 --- 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 b362b0a61e3..12925dea143 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 = 4 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __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, 0) diff --git a/pyproject.toml b/pyproject.toml index 8966c2fd9b2..0c5a974a2aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.0" +version = "2025.4.1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 8d62cb60a694fd4a6497bc38b14a693b91918fea Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Fri, 4 Apr 2025 12:32:14 +0200 Subject: [PATCH 172/258] Fix skyconnect tests (#142262) fix tests --- tests/components/zha/test_repairs.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index af81ac0d586..0ff863f0c45 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -49,7 +49,8 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None: "product": "SkyConnect v1.0", "firmware": "ezsp", }, - version=2, + version=1, + minor_version=4, domain=SKYCONNECT_DOMAIN, options={}, title="Home Assistant SkyConnect", @@ -66,7 +67,8 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None: "product": "Home Assistant Connect ZBT-1", "firmware": "ezsp", }, - version=2, + version=1, + minor_version=4, domain=SKYCONNECT_DOMAIN, options={}, title="Home Assistant Connect ZBT-1", From c25f26a2901b3d358cc1c40a8c6e10392f27c33d Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:17:52 -0400 Subject: [PATCH 173/258] Fix empty actions (#142292) * Apply fix * Add tests for alarm button cover lock * update light * add number tests * test select * add switch tests * test vacuum * update lock test --- .../template/alarm_control_panel.py | 3 +- homeassistant/components/template/button.py | 3 +- homeassistant/components/template/cover.py | 3 +- homeassistant/components/template/fan.py | 3 +- homeassistant/components/template/light.py | 8 +- homeassistant/components/template/lock.py | 3 +- homeassistant/components/template/select.py | 6 +- homeassistant/components/template/switch.py | 5 +- homeassistant/components/template/vacuum.py | 3 +- .../template/test_alarm_control_panel.py | 15 +++ tests/components/template/test_button.py | 39 ++++++ tests/components/template/test_cover.py | 49 +++++++ tests/components/template/test_light.py | 115 ++++++++++++++++ tests/components/template/test_lock.py | 51 ++++++- tests/components/template/test_number.py | 76 ++++++++++- tests/components/template/test_select.py | 77 ++++++++++- tests/components/template/test_switch.py | 46 +++++++ tests/components/template/test_vacuum.py | 127 +++++++++++++++++- 18 files changed, 613 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 40206a5ccbb..208077a4153 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -214,7 +214,8 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore ), (CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER), ): - if action_config := config.get(action_id): + # 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 diff --git a/homeassistant/components/template/button.py b/homeassistant/components/template/button.py index 7a205446585..4ee8844d6e7 100644 --- a/homeassistant/components/template/button.py +++ b/homeassistant/components/template/button.py @@ -120,7 +120,8 @@ class TemplateButtonEntity(TemplateEntity, ButtonEntity): """Initialize the button.""" super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None - if action := config.get(CONF_PRESS): + # Scripts can be an empty list, therefore we need to check for None + if (action := config.get(CONF_PRESS)) is not None: self.add_script(CONF_PRESS, action, self._attr_name, DOMAIN) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_state = None diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 7a8e347ee8f..7c9c0ea9d53 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -172,7 +172,8 @@ class CoverTemplate(TemplateEntity, CoverEntity): (POSITION_ACTION, CoverEntityFeature.SET_POSITION), (TILT_ACTION, TILT_FEATURES), ): - if action_config := config.get(action_id): + # 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 diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 6e0f9fe5e0c..f3bc26391a9 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -157,7 +157,8 @@ class TemplateFan(TemplateEntity, FanEntity): CONF_SET_OSCILLATING_ACTION, CONF_SET_DIRECTION_ACTION, ): - if action_config := config.get(action_id): + # 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._state: bool | None = False diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 1cc47c74aa0..c58709eba5e 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -296,7 +296,8 @@ class LightTemplate(TemplateEntity, LightEntity): self._supports_transition_template = config.get(CONF_SUPPORTS_TRANSITION) for action_id in (CONF_ON_ACTION, CONF_OFF_ACTION, CONF_EFFECT_ACTION): - if action_config := config.get(action_id): + # 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._state = False @@ -323,7 +324,8 @@ class LightTemplate(TemplateEntity, LightEntity): (CONF_RGBW_ACTION, ColorMode.RGBW), (CONF_RGBWW_ACTION, ColorMode.RGBWW), ): - if action_config := config.get(action_id): + # 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) color_modes.add(color_mode) self._supported_color_modes = filter_supported_color_modes(color_modes) @@ -333,7 +335,7 @@ class LightTemplate(TemplateEntity, LightEntity): self._color_mode = next(iter(self._supported_color_modes)) self._attr_supported_features = LightEntityFeature(0) - if self._action_scripts.get(CONF_EFFECT_ACTION): + if (self._action_scripts.get(CONF_EFFECT_ACTION)) is not None: self._attr_supported_features |= LightEntityFeature.EFFECT if self._supports_transition is True: self._attr_supported_features |= LightEntityFeature.TRANSITION diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index b19cadff26c..12a3e66cb5e 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -98,7 +98,8 @@ class TemplateLock(TemplateEntity, LockEntity): (CONF_UNLOCK, 0), (CONF_OPEN, LockEntityFeature.OPEN), ): - if action_config := config.get(action_id): + # 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_TEMPLATE) diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index eb60a3dbfe4..74d88ee96c4 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -141,7 +141,8 @@ class TemplateSelect(TemplateEntity, SelectEntity): super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None self._value_template = config[CONF_STATE] - if select_option := config.get(CONF_SELECT_OPTION): + # Scripts can be an empty list, therefore we need to check for None + if (select_option := config.get(CONF_SELECT_OPTION)) is not None: self.add_script(CONF_SELECT_OPTION, select_option, self._attr_name, DOMAIN) self._options_template = config[ATTR_OPTIONS] self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC, False) @@ -197,7 +198,8 @@ class TriggerSelectEntity(TriggerEntity, SelectEntity): ) -> None: """Initialize the entity.""" super().__init__(hass, coordinator, config) - if select_option := config.get(CONF_SELECT_OPTION): + # Scripts can be an empty list, therefore we need to check for None + if (select_option := config.get(CONF_SELECT_OPTION)) is not None: self.add_script( CONF_SELECT_OPTION, select_option, diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index fb3aeb1e42a..1d18ea9d5ca 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -226,9 +226,10 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): assert name is not None self._template = config.get(CONF_STATE) - if on_action := config.get(CONF_TURN_ON): + # Scripts can be an empty list, therefore we need to check for None + if (on_action := config.get(CONF_TURN_ON)) is not None: self.add_script(CONF_TURN_ON, on_action, name, DOMAIN) - if off_action := config.get(CONF_TURN_OFF): + if (off_action := config.get(CONF_TURN_OFF)) is not None: self.add_script(CONF_TURN_OFF, off_action, name, DOMAIN) self._state: bool | None = False diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index c4d41b52f31..1e18b06436a 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -158,7 +158,8 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): (SERVICE_LOCATE, VacuumEntityFeature.LOCATE), (SERVICE_SET_FAN_SPEED, VacuumEntityFeature.FAN_SPEED), ): - if action_config := config.get(action_id): + # 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 diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 4b259fabac2..2a99e00a9ce 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -82,6 +82,15 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { "data": {"code": "{{ this.entity_id }}"}, }, } +EMPTY_ACTIONS = { + "arm_away": [], + "arm_home": [], + "arm_night": [], + "arm_vacation": [], + "arm_custom_bypass": [], + "disarm": [], + "trigger": [], +} TEMPLATE_ALARM_CONFIG = { @@ -173,6 +182,12 @@ async def test_setup_config_entry( "panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG}, } }, + { + "alarm_control_panel": { + "platform": "template", + "panels": {"test_template_panel": EMPTY_ACTIONS}, + } + }, ], ) @pytest.mark.usefixtures("start_ha") diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index b201385240c..31239dbaf92 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -93,6 +93,45 @@ async def test_missing_optional_config(hass: HomeAssistant) -> None: _verify(hass, STATE_UNKNOWN) +async def test_missing_emtpy_press_action_config( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test: missing optional template is ok.""" + with assert_setup_component(1, "template"): + assert await setup.async_setup_component( + hass, + "template", + { + "template": { + "button": { + "press": [], + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_UNKNOWN) + + now = dt.datetime.now(dt.UTC) + freezer.move_to(now) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {CONF_ENTITY_ID: _TEST_BUTTON}, + blocking=True, + ) + + _verify( + hass, + now.isoformat(), + ) + + async def test_missing_required_keys(hass: HomeAssistant) -> None: """Test: missing required fields will fail.""" with assert_setup_component(0, "template"): diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index c49db59c2ee..668592e388b 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN as COVER_DOMAIN, + CoverEntityFeature, CoverState, ) from homeassistant.const import ( @@ -28,6 +29,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.setup import async_setup_component from tests.common import assert_setup_component @@ -1123,3 +1125,50 @@ async def test_self_referencing_icon_with_no_template_is_not_a_loop( assert len(hass.states.async_all()) == 1 assert "Template loop detected" not in caplog.text + + +@pytest.mark.parametrize( + ("script", "supported_feature"), + [ + ("stop_cover", CoverEntityFeature.STOP), + ("set_cover_position", CoverEntityFeature.SET_POSITION), + ( + "set_cover_tilt_position", + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION, + ), + ], +) +async def test_emtpy_action_config( + hass: HomeAssistant, script: str, 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"] + == CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | supported_feature + ) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 1a739b4921e..c0aade84e0f 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -556,6 +556,42 @@ async def setup_single_action_light( ) +@pytest.fixture +async def setup_empty_action_light( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + action: str, + extra_config: dict, +) -> None: + """Do setup of light integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + "test_template_light": { + "turn_on": [], + "turn_off": [], + action: [], + **extra_config, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_new_format( + hass, + count, + { + "name": "test_template_light", + "turn_on": [], + "turn_off": [], + action: [], + **extra_config, + }, + ) + + @pytest.fixture async def setup_light_with_effects( hass: HomeAssistant, @@ -2404,3 +2440,82 @@ async def test_nested_unique_id( entry = entity_registry.async_get("light.test_b") assert entry assert entry.unique_id == "x-b" + + +@pytest.mark.parametrize(("count", "extra_config"), [(1, {})]) +@pytest.mark.parametrize( + "style", + [ + ConfigurationStyle.LEGACY, + ConfigurationStyle.MODERN, + ], +) +@pytest.mark.parametrize( + ("action", "color_mode"), + [ + ("set_level", ColorMode.BRIGHTNESS), + ("set_temperature", ColorMode.COLOR_TEMP), + ("set_hs", ColorMode.HS), + ("set_rgb", ColorMode.RGB), + ("set_rgbw", ColorMode.RGBW), + ("set_rgbww", ColorMode.RGBWW), + ], +) +async def test_empty_color_mode_action_config( + hass: HomeAssistant, + color_mode: ColorMode, + setup_empty_action_light, +) -> None: + """Test empty actions for color mode actions.""" + state = hass.states.get("light.test_template_light") + assert state.attributes["supported_color_modes"] == [color_mode] + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light"}, + blocking=True, + ) + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_ON + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_template_light"}, + blocking=True, + ) + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_OFF + + +@pytest.mark.parametrize(("count"), [1]) +@pytest.mark.parametrize( + ("style", "extra_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "effect_list_template": "{{ ['a'] }}", + "effect_template": "{{ 'a' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "effect_list": "{{ ['a'] }}", + "effect": "{{ 'a' }}", + }, + ), + ], +) +@pytest.mark.parametrize("action", ["set_effect"]) +async def test_effect_with_empty_action( + hass: HomeAssistant, + setup_empty_action_light, +) -> None: + """Test empty set_effect action.""" + state = hass.states.get("light.test_template_light") + assert state.attributes["supported_features"] == LightEntityFeature.EFFECT diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index d9cb294c41f..50baa11b2d0 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -4,7 +4,7 @@ import pytest from homeassistant import setup from homeassistant.components import lock -from homeassistant.components.lock import LockState +from homeassistant.components.lock import LockEntityFeature, LockState from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -15,6 +15,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall +from tests.common import assert_setup_component + OPTIMISTIC_LOCK_CONFIG = { "platform": "template", "lock": { @@ -718,3 +720,50 @@ async def test_unique_id(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.states.async_all("lock")) == 1 + + +async def test_emtpy_action_config(hass: HomeAssistant) -> None: + """Test configuration with empty script.""" + with assert_setup_component(1, lock.DOMAIN): + assert await setup.async_setup_component( + hass, + lock.DOMAIN, + { + lock.DOMAIN: { + "platform": "template", + "value_template": "{{ 0 == 1 }}", + "lock": [], + "unlock": [], + "open": [], + "name": "test_template_lock", + "optimistic": True, + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("lock.test_template_lock") + assert state.attributes["supported_features"] == LockEntityFeature.OPEN + + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.test_template_lock"}, + ) + await hass.async_block_till_done() + + state = hass.states.get("lock.test_template_lock") + assert state.state == LockState.UNLOCKED + + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.test_template_lock"}, + ) + await hass.async_block_till_done() + + state = hass.states.get("lock.test_template_lock") + assert state.state == LockState.LOCKED diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index f73a943e752..5201541e2e0 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -1,8 +1,12 @@ """The tests for the Template number platform.""" +from typing import Any + +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import setup +from homeassistant.components import number, template from homeassistant.components.input_number import ( ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE, DOMAIN as INPUT_NUMBER_DOMAIN, @@ -18,6 +22,7 @@ from homeassistant.components.number import ( ) from homeassistant.components.template import DOMAIN from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ICON, CONF_ENTITY_ID, CONF_UNIT_OF_MEASUREMENT, @@ -25,10 +30,14 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HomeAssistant, ServiceCall 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, async_capture_events -_TEST_NUMBER = "number.template_number" +_TEST_OBJECT_ID = "template_number" +_TEST_NUMBER = f"number.{_TEST_OBJECT_ID}" # Represent for number's value _VALUE_INPUT_NUMBER = "input_number.value" # Represent for number's minimum @@ -50,6 +59,38 @@ _VALUE_INPUT_NUMBER_CONFIG = { } +async def async_setup_modern_format( + hass: HomeAssistant, count: int, number_config: dict[str, Any] +) -> None: + """Do setup of number integration via new format.""" + config = {"template": {"number": number_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_number( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + number_config: dict[str, Any], +) -> None: + """Do setup of number integration.""" + if style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, count, {"name": _TEST_OBJECT_ID, **number_config} + ) + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -565,3 +606,36 @@ async def test_device_id( template_entity = entity_registry.async_get("number.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("count", "number_config"), + [ + ( + 1, + { + "state": "{{ 1 }}", + "set_value": [], + "step": "{{ 1 }}", + "optimistic": True, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ + ConfigurationStyle.MODERN, + ], +) +async def test_empty_action_config(hass: HomeAssistant, setup_number) -> None: + """Test configuration with empty script.""" + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: _TEST_NUMBER, "value": 4}, + blocking=True, + ) + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 4 diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index 59ab45aeb36..b2bc56af44a 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -1,8 +1,12 @@ """The tests for the Template select platform.""" +from typing import Any + +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import setup +from homeassistant.components import select, template from homeassistant.components.input_select import ( ATTR_OPTION as INPUT_SELECT_ATTR_OPTION, ATTR_OPTIONS as INPUT_SELECT_ATTR_OPTIONS, @@ -17,17 +21,53 @@ from homeassistant.components.select import ( SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION, ) from homeassistant.components.template import DOMAIN -from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context, HomeAssistant, ServiceCall 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, async_capture_events -_TEST_SELECT = "select.template_select" +_TEST_OBJECT_ID = "template_select" +_TEST_SELECT = f"select.{_TEST_OBJECT_ID}" # Represent for select's current_option _OPTION_INPUT_SELECT = "input_select.option" +async def async_setup_modern_format( + hass: HomeAssistant, count: int, select_config: dict[str, Any] +) -> None: + """Do setup of select integration via new format.""" + config = {"template": {"select": select_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_select( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + select_config: dict[str, Any], +) -> None: + """Do setup of select integration.""" + if style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, count, {"name": _TEST_OBJECT_ID, **select_config} + ) + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -527,3 +567,36 @@ async def test_device_id( template_entity = entity_registry.async_get("select.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("count", "select_config"), + [ + ( + 1, + { + "state": "{{ 'b' }}", + "select_option": [], + "options": "{{ ['a', 'b'] }}", + "optimistic": True, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ + ConfigurationStyle.MODERN, + ], +) +async def test_empty_action_config(hass: HomeAssistant, setup_select) -> None: + """Test configuration with empty script.""" + await hass.services.async_call( + select.DOMAIN, + select.SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: _TEST_SELECT, "option": "a"}, + blocking=True, + ) + + state = hass.states.get(_TEST_SELECT) + assert state.state == "a" diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index d8877851efe..43db93ac146 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -981,3 +981,49 @@ async def test_device_id( template_entity = entity_registry.async_get("switch.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "switch_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + TEST_OBJECT_ID: { + "turn_on": [], + "turn_off": [], + }, + }, + ), + ( + ConfigurationStyle.MODERN, + { + "name": TEST_OBJECT_ID, + "turn_on": [], + "turn_off": [], + }, + ), + ], +) +async def test_empty_action_config(hass: HomeAssistant, setup_switch) -> None: + """Test configuration with empty script.""" + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 6053a2bd9ec..cc5bc9b39e3 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -1,18 +1,29 @@ """The tests for the Template vacuum platform.""" +from typing import Any + import pytest from homeassistant import setup -from homeassistant.components.vacuum import ATTR_BATTERY_LEVEL, VacuumActivity +from homeassistant.components import vacuum +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, + 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.entity_component import async_update_entity +from homeassistant.setup import async_setup_component + +from .conftest import ConfigurationStyle from tests.common import assert_setup_component from tests.components.vacuum import common -_TEST_VACUUM = "vacuum.test_vacuum" +_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" @@ -20,6 +31,50 @@ _FAN_SPEED_INPUT_SELECT = "input_select.fan_speed" _BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level" +async def async_setup_legacy_format( + hass: HomeAssistant, count: int, vacuum_config: dict[str, Any] +) -> None: + """Do setup of number integration via new format.""" + config = {"vacuum": {"platform": "template", "vacuums": vacuum_config}} + + with assert_setup_component(count, vacuum.DOMAIN): + assert await async_setup_component( + hass, + vacuum.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, + count: int, + style: ConfigurationStyle, + vacuum_config: dict[str, Any], +) -> None: + """Do setup of number integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format(hass, count, vacuum_config) + + +@pytest.fixture +async def setup_test_vacuum_with_extra_config( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + vacuum_config: dict[str, Any], + 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) + + @pytest.mark.parametrize(("count", "domain"), [(1, "vacuum")]) @pytest.mark.parametrize( ("parm1", "parm2", "config"), @@ -697,3 +752,71 @@ async def _register_components(hass: HomeAssistant) -> None: 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"), + [ + ( + ConfigurationStyle.LEGACY, + { + "start": [], + }, + ), + ], +) +@pytest.mark.parametrize( + ("extra_config", "supported_features"), + [ + ( + { + "pause": [], + }, + VacuumEntityFeature.PAUSE, + ), + ( + { + "stop": [], + }, + VacuumEntityFeature.STOP, + ), + ( + { + "return_to_base": [], + }, + VacuumEntityFeature.RETURN_HOME, + ), + ( + { + "clean_spot": [], + }, + VacuumEntityFeature.CLEAN_SPOT, + ), + ( + { + "locate": [], + }, + VacuumEntityFeature.LOCATE, + ), + ( + { + "set_fan_speed": [], + }, + VacuumEntityFeature.FAN_SPEED, + ), + ], +) +async def test_empty_action_config( + hass: HomeAssistant, + supported_features: VacuumEntityFeature, + setup_test_vacuum_with_extra_config, +) -> None: + """Test configuration with empty script.""" + await common.async_start(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + state = hass.states.get(_TEST_VACUUM) + assert state.attributes["supported_features"] == ( + VacuumEntityFeature.STATE | VacuumEntityFeature.START | supported_features + ) From ee5e3f7691183025090f711335681d0ee1588146 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:07:06 +0200 Subject: [PATCH 174/258] Add error details in remote calendar flow (#141753) * Add error details in remote calendar flow * no args * adjust * json * Apply suggestions * remove description placeholder --- homeassistant/components/remote_calendar/config_flow.py | 5 ++++- homeassistant/components/remote_calendar/strings.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/remote_calendar/config_flow.py b/homeassistant/components/remote_calendar/config_flow.py index cc9f45e2767..802a7eb7cea 100644 --- a/homeassistant/components/remote_calendar/config_flow.py +++ b/homeassistant/components/remote_calendar/config_flow.py @@ -69,7 +69,10 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN): ) except CalendarParseError as err: errors["base"] = "invalid_ics_file" - _LOGGER.debug("Invalid .ics file: %s", err) + _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/strings.json b/homeassistant/components/remote_calendar/strings.json index fff2d4abbb3..ef7f20d4699 100644 --- a/homeassistant/components/remote_calendar/strings.json +++ b/homeassistant/components/remote_calendar/strings.json @@ -20,7 +20,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "forbidden": "The server understood the request but refuses to authorize it.", - "invalid_ics_file": "[%key:component::local_calendar::config::error::invalid_ics_file%]" + "invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details." } }, "exceptions": { From b38016425f6427d45294326ead7673d81e992cba Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Fri, 4 Apr 2025 22:58:46 -0400 Subject: [PATCH 175/258] Update Roborock map more consistently on state change (#142228) * update map more consistently on state change * Makecoordinator keep track of last_updated_state --- homeassistant/components/roborock/coordinator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index cc0bee1cd5f..4e59a092e0a 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -153,6 +153,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): ImageConfig(scale=MAP_SCALE), [], ) + self.last_update_state: str | None = None @cached_property def dock_device_info(self) -> DeviceInfo: @@ -291,7 +292,6 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): async def _async_update_data(self) -> DeviceProp: """Update data via library.""" - previous_state = self.roborock_device_info.props.status.state_name try: # Update device props and standard api information await self._update_device_prop() @@ -308,7 +308,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): and (dt_util.utcnow() - self.maps[self.current_map].last_updated) > IMAGE_CACHE_INTERVAL ) - or previous_state != new_status.state_name + or self.last_update_state != new_status.state_name ): try: await self.update_map() @@ -330,6 +330,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): self.update_interval = V1_CLOUD_NOT_CLEANING_INTERVAL else: self.update_interval = V1_LOCAL_NOT_CLEANING_INTERVAL + self.last_update_state = self.roborock_device_info.props.status.state_name return self.roborock_device_info.props def _set_current_map(self) -> None: From e90ba4055385dc5f705d95e25b4836e49ddeb9b4 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 5 Apr 2025 20:48:16 +0200 Subject: [PATCH 176/258] Add SensorDeviceClass and unit for LCN CO2 sensor. (#142320) Add SesnorDeviceClass and unit for LCN CO2 sensor. --- homeassistant/components/lcn/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 7783df8679a..0c78ea6637a 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE, @@ -49,6 +50,7 @@ DEVICE_CLASS_MAPPING = { pypck.lcn_defs.VarUnit.METERPERSECOND: SensorDeviceClass.SPEED, pypck.lcn_defs.VarUnit.VOLT: SensorDeviceClass.VOLTAGE, pypck.lcn_defs.VarUnit.AMPERE: SensorDeviceClass.CURRENT, + pypck.lcn_defs.VarUnit.PPM: SensorDeviceClass.CO2, } UNIT_OF_MEASUREMENT_MAPPING = { @@ -60,6 +62,7 @@ UNIT_OF_MEASUREMENT_MAPPING = { pypck.lcn_defs.VarUnit.METERPERSECOND: UnitOfSpeed.METERS_PER_SECOND, pypck.lcn_defs.VarUnit.VOLT: UnitOfElectricPotential.VOLT, pypck.lcn_defs.VarUnit.AMPERE: UnitOfElectricCurrent.AMPERE, + pypck.lcn_defs.VarUnit.PPM: CONCENTRATION_PARTS_PER_MILLION, } From 8baf61031d7bd1f2c741d48a2dc2e6752be8eeb1 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 5 Apr 2025 02:03:20 -0700 Subject: [PATCH 177/258] Bump opower to 0.10.0 (#142321) --- 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 2da4511c0aa..e691d01257a 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.9.0"] + "requirements": ["opower==0.10.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b6ee4cc0c00..3c74f269f59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1607,7 +1607,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.9.0 +opower==0.10.0 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f86b6bba123..bcaf704c4fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1341,7 +1341,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.9.0 +opower==0.10.0 # homeassistant.components.oralb oralb-ble==0.17.6 From 5f262267123aa6d9484d360358adbf50a627215e Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 5 Apr 2025 11:49:38 -0700 Subject: [PATCH 178/258] Add a description for the enable_google_search_tool option in Google AI (#142322) * Add a description for the enable_google_search_tool option in Google AI * Use quotes --- .../components/google_generative_ai_conversation/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_generative_ai_conversation/strings.json b/homeassistant/components/google_generative_ai_conversation/strings.json index cd7af711d3a..2697f30eda0 100644 --- a/homeassistant/components/google_generative_ai_conversation/strings.json +++ b/homeassistant/components/google_generative_ai_conversation/strings.json @@ -40,7 +40,8 @@ "enable_google_search_tool": "Enable Google Search tool" }, "data_description": { - "prompt": "Instruct how the LLM should respond. This can be a template." + "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\"." } } }, From cdd4894e3059a30ad0ebb456556e6731ec7eb660 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Sat, 5 Apr 2025 19:45:00 -0400 Subject: [PATCH 179/258] Check that the current roboorck map exists before updating it. (#142341) * Check that the current map exists * Add a few extra checks * Update coordinator.py Co-authored-by: Allen Porter * fixlint --------- Co-authored-by: Allen Porter --- .../components/roborock/coordinator.py | 18 +++++++++++------- homeassistant/components/roborock/sensor.py | 10 ++++++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 4e59a092e0a..2439a4f904a 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -226,7 +226,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): """Update the currently selected map.""" # The current map was set in the props update, so these can be done without # worry of applying them to the wrong map. - if self.current_map is None: + if self.current_map is None or self.current_map not in self.maps: # This exists as a safeguard/ to keep mypy happy. return try: @@ -302,13 +302,17 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): # If the vacuum is currently cleaning and it has been IMAGE_CACHE_INTERVAL # since the last map update, you can update the map. new_status = self.roborock_device_info.props.status - if self.current_map is not None and ( - ( - new_status.in_cleaning - and (dt_util.utcnow() - self.maps[self.current_map].last_updated) - > IMAGE_CACHE_INTERVAL + if ( + self.current_map is not None + and (current_map := self.maps.get(self.current_map)) + and ( + ( + new_status.in_cleaning + and (dt_util.utcnow() - current_map.last_updated) + > IMAGE_CACHE_INTERVAL + ) + or self.last_update_state != new_status.state_name ) - or self.last_update_state != new_status.state_name ): try: await self.update_map() diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 33ecaf74d4f..a007d6fa457 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -381,7 +381,10 @@ class RoborockCurrentRoom(RoborockCoordinatedEntityV1, SensorEntity): @property def options(self) -> list[str]: """Return the currently valid rooms.""" - if self.coordinator.current_map is not None: + if ( + self.coordinator.current_map is not None + and self.coordinator.current_map in self.coordinator.maps + ): return list( self.coordinator.maps[self.coordinator.current_map].rooms.values() ) @@ -390,7 +393,10 @@ class RoborockCurrentRoom(RoborockCoordinatedEntityV1, SensorEntity): @property def native_value(self) -> str | None: """Return the value reported by the sensor.""" - if self.coordinator.current_map is not None: + if ( + self.coordinator.current_map is not None + and self.coordinator.current_map in self.coordinator.maps + ): return self.coordinator.maps[self.coordinator.current_map].current_room return None From 2c855a398616d0f2e6b845ca9e936395e211c872 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 5 Apr 2025 22:38:11 +0200 Subject: [PATCH 180/258] Limit mqtt info logging for discovery of new components (#142344) * Limit mqtt info logging for discovery of new component * Keep in bail out, when debug logging is not enabled --- homeassistant/components/mqtt/discovery.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index a14240ce008..a527e712615 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -154,18 +154,14 @@ def get_origin_support_url(discovery_payload: MQTTDiscoveryPayload) -> str | Non @callback def async_log_discovery_origin_info( - message: str, discovery_payload: MQTTDiscoveryPayload, level: int = logging.INFO + message: str, discovery_payload: MQTTDiscoveryPayload ) -> None: """Log information about the discovery and origin.""" - # We only log origin info once per device discovery - if not _LOGGER.isEnabledFor(level): - # bail out early if logging is disabled + if not _LOGGER.isEnabledFor(logging.DEBUG): + # bail out early if debug logging is disabled return - _LOGGER.log( - level, - "%s%s", - message, - get_origin_log_string(discovery_payload, include_url=True), + _LOGGER.debug( + "%s%s", message, get_origin_log_string(discovery_payload, include_url=True) ) @@ -562,7 +558,7 @@ async def async_start( # noqa: C901 elif already_discovered: # Dispatch update message = f"Component has already been discovered: {component} {discovery_id}, sending update" - async_log_discovery_origin_info(message, payload, logging.DEBUG) + async_log_discovery_origin_info(message, payload) async_dispatcher_send( hass, MQTT_DISCOVERY_UPDATED.format(*discovery_hash), payload ) From 9fb7ee676e0657fce0401c9d8ae42e2be7be281b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Apr 2025 11:07:21 -1000 Subject: [PATCH 181/258] Bump flux_led to 1.2.0 (#142362) changelog: https://github.com/lightinglibs/flux_led/compare/1.1.3...1.2.0 --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index fcb16c9742b..2c5e1b3839e 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -53,5 +53,5 @@ "documentation": "https://www.home-assistant.io/integrations/flux_led", "iot_class": "local_push", "loggers": ["flux_led"], - "requirements": ["flux-led==1.1.3"] + "requirements": ["flux-led==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3c74f269f59..4cedaaaa6ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -944,7 +944,7 @@ flexit_bacnet==2.2.3 flipr-api==1.6.1 # homeassistant.components.flux_led -flux-led==1.1.3 +flux-led==1.2.0 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcaf704c4fd..a5a15d6a7b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -804,7 +804,7 @@ flexit_bacnet==2.2.3 flipr-api==1.6.1 # homeassistant.components.flux_led -flux-led==1.1.3 +flux-led==1.2.0 # homeassistant.components.homekit # homeassistant.components.recorder From 8ff879df22353eb52e65c69e328d50fc61810b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 6 Apr 2025 10:01:26 +0200 Subject: [PATCH 182/258] Update aioairzone to v1.0.0 (#142385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../airzone/snapshots/test_diagnostics.ambr | 16 ++++++++++++++++ tests/components/airzone/util.py | 10 ++++++++++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 95ed9d200f4..1b636de0a47 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.9.9"] + "requirements": ["aioairzone==1.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4cedaaaa6ca..1363bbd3721 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairq==0.4.4 aioairzone-cloud==0.6.11 # homeassistant.components.airzone -aioairzone==0.9.9 +aioairzone==1.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5a15d6a7b5..4b41b99bd62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairq==0.4.4 aioairzone-cloud==0.6.11 # homeassistant.components.airzone -aioairzone==0.9.9 +aioairzone==1.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr index b4976c07e1b..09dea8c354c 100644 --- a/tests/components/airzone/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -44,9 +44,11 @@ }), dict({ 'air_demand': 1, + 'battery': 99, 'coldStage': 1, 'coldStages': 1, 'coldangle': 2, + 'coverage': 72, 'errors': list([ ]), 'floor_demand': 1, @@ -73,9 +75,11 @@ }), dict({ 'air_demand': 0, + 'battery': 35, 'coldStage': 1, 'coldStages': 1, 'coldangle': 0, + 'coverage': 60, 'errors': list([ ]), 'floor_demand': 0, @@ -100,9 +104,11 @@ }), dict({ 'air_demand': 0, + 'battery': 25, 'coldStage': 1, 'coldStages': 1, 'coldangle': 0, + 'coverage': 88, 'errors': list([ dict({ 'Zone': 'Low battery', @@ -130,9 +136,11 @@ }), dict({ 'air_demand': 0, + 'battery': 80, 'coldStage': 1, 'coldStages': 1, 'coldangle': 0, + 'coverage': 66, 'errors': list([ ]), 'floor_demand': 0, @@ -497,9 +505,11 @@ 'temp-set': 19.2, 'temp-step': 0.5, 'temp-unit': 0, + 'thermostat-battery': 99, 'thermostat-fw': '3.33', 'thermostat-model': 'Think (Radio)', 'thermostat-radio': True, + 'thermostat-signal': 72, }), '1:3': dict({ 'absolute-temp-max': 30.0, @@ -546,9 +556,11 @@ 'temp-set': 19.3, 'temp-step': 0.5, 'temp-unit': 0, + 'thermostat-battery': 35, 'thermostat-fw': '3.33', 'thermostat-model': 'Think (Radio)', 'thermostat-radio': True, + 'thermostat-signal': 60, }), '1:4': dict({ 'absolute-temp-max': 86.0, @@ -597,9 +609,11 @@ 'temp-set': 66.9, 'temp-step': 1.0, 'temp-unit': 1, + 'thermostat-battery': 25, 'thermostat-fw': '3.33', 'thermostat-model': 'Think (Radio)', 'thermostat-radio': True, + 'thermostat-signal': 88, }), '1:5': dict({ 'absolute-temp-max': 30.0, @@ -645,9 +659,11 @@ 'temp-set': 19.5, 'temp-step': 0.5, 'temp-unit': 0, + 'thermostat-battery': 80, 'thermostat-fw': '3.33', 'thermostat-model': 'Think (Radio)', 'thermostat-radio': True, + 'thermostat-signal': 66, }), '2:1': dict({ 'absolute-temp-max': 30.0, diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 50d1964924d..944ca83d053 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -11,12 +11,14 @@ from aioairzone.const import ( API_ACS_SET_POINT, API_ACS_TEMP, API_AIR_DEMAND, + API_BATTERY, API_COLD_ANGLE, API_COLD_STAGE, API_COLD_STAGES, API_COOL_MAX_TEMP, API_COOL_MIN_TEMP, API_COOL_SET_POINT, + API_COVERAGE, API_DATA, API_ERRORS, API_FLOOR_DEMAND, @@ -119,6 +121,8 @@ HVAC_MOCK = { API_THERMOS_TYPE: 4, API_THERMOS_FIRMWARE: "3.33", API_THERMOS_RADIO: 1, + API_BATTERY: 99, + API_COVERAGE: 72, API_ON: 1, API_MAX_TEMP: 30, API_MIN_TEMP: 15, @@ -147,6 +151,8 @@ HVAC_MOCK = { API_THERMOS_TYPE: 4, API_THERMOS_FIRMWARE: "3.33", API_THERMOS_RADIO: 1, + API_BATTERY: 35, + API_COVERAGE: 60, API_ON: 1, API_MAX_TEMP: 30, API_MIN_TEMP: 15, @@ -173,6 +179,8 @@ HVAC_MOCK = { API_THERMOS_TYPE: 4, API_THERMOS_FIRMWARE: "3.33", API_THERMOS_RADIO: 1, + API_BATTERY: 25, + API_COVERAGE: 88, API_ON: 0, API_MAX_TEMP: 86, API_MIN_TEMP: 59, @@ -203,6 +211,8 @@ HVAC_MOCK = { API_THERMOS_TYPE: 4, API_THERMOS_FIRMWARE: "3.33", API_THERMOS_RADIO: 1, + API_BATTERY: 80, + API_COVERAGE: 66, API_ON: 0, API_MAX_TEMP: 30, API_MIN_TEMP: 15, From 5d92a047326c86d109d7dd4369785a71d3b1bf73 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 6 Apr 2025 10:04:18 +0200 Subject: [PATCH 183/258] Only load files ending `.metadata.json` in WebDAV (#142388) --- homeassistant/components/webdav/backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webdav/backup.py b/homeassistant/components/webdav/backup.py index fb2927a58bb..a9afb5fe930 100644 --- a/homeassistant/components/webdav/backup.py +++ b/homeassistant/components/webdav/backup.py @@ -231,7 +231,7 @@ class WebDavBackupAgent(BackupAgent): return { metadata_content.backup_id: metadata_content for file_name in files - if file_name.endswith(".json") + if file_name.endswith(".metadata.json") if (metadata_content := await _download_metadata(file_name)) } From b7f0e877f0b339a073b008d30b93872ea589bec0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Apr 2025 01:00:41 -1000 Subject: [PATCH 184/258] Bump aioesphomeapi to 29.9.0 (#142393) changelog: https://github.com/esphome/aioesphomeapi/compare/v29.8.0...v29.9.0 fixes #142381 --- homeassistant/components/esphome/manager.py | 4 ++-- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 7ce96a0f510..56c2998a3cc 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -13,7 +13,7 @@ from aioesphomeapi import ( APIConnectionError, APIVersion, DeviceInfo as EsphomeDeviceInfo, - EncryptionHelloAPIError, + EncryptionPlaintextAPIError, EntityInfo, HomeassistantServiceCall, InvalidAuthAPIError, @@ -571,7 +571,7 @@ class ESPHomeManager: if isinstance( err, ( - EncryptionHelloAPIError, + EncryptionPlaintextAPIError, RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError, InvalidAuthAPIError, diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 954968f5e2c..31ed27c02ae 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], "requirements": [ - "aioesphomeapi==29.8.0", + "aioesphomeapi==29.9.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==2.12.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 1363bbd3721..9c678a8b10b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==29.8.0 +aioesphomeapi==29.9.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b41b99bd62..7319b2a3e69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==29.8.0 +aioesphomeapi==29.9.0 # homeassistant.components.flo aioflo==2021.11.0 From 87baaf4255c8e2d1b1a52abc662a6740a4c1c36e Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 7 Apr 2025 23:58:43 -0700 Subject: [PATCH 185/258] Bump opower to 0.11.1 (#142395) * Bump opower to 0.10.1 * opower==0.11.0 * opower==0.11.1 --------- Co-authored-by: J. Nick Koston --- 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 e691d01257a..2cc942363cf 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.10.0"] + "requirements": ["opower==0.11.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9c678a8b10b..69d03f97152 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1607,7 +1607,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.10.0 +opower==0.11.1 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7319b2a3e69..be52eef360c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1341,7 +1341,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.10.0 +opower==0.11.1 # homeassistant.components.oralb oralb-ble==0.17.6 From 2322b0b65fdd12092522ef988a778e2f3ea1cd75 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 11 Apr 2025 11:32:19 +0200 Subject: [PATCH 186/258] Add exceptions translation to SamsungTV (#142406) * Add exceptions translation to SmasungTV * Update strings.json Co-authored-by: Franck Nijhof --------- Co-authored-by: Franck Nijhof --- homeassistant/components/samsungtv/device_trigger.py | 7 ++++++- homeassistant/components/samsungtv/entity.py | 4 +++- homeassistant/components/samsungtv/strings.json | 8 ++++++++ tests/components/samsungtv/test_remote.py | 7 ++++++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/device_trigger.py b/homeassistant/components/samsungtv/device_trigger.py index 2b3d9dbe666..749276b61c4 100644 --- a/homeassistant/components/samsungtv/device_trigger.py +++ b/homeassistant/components/samsungtv/device_trigger.py @@ -15,6 +15,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import trigger +from .const import DOMAIN from .helpers import ( async_get_client_by_device_entry, async_get_device_entry_by_device_id, @@ -75,4 +76,8 @@ async def async_attach_trigger( hass, trigger_config, action, trigger_info ) - raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="unhandled_trigger_type", + translation_placeholders={"trigger_type": trigger_type}, + ) diff --git a/homeassistant/components/samsungtv/entity.py b/homeassistant/components/samsungtv/entity.py index 61aa8abce53..f3ecee373e3 100644 --- a/homeassistant/components/samsungtv/entity.py +++ b/homeassistant/components/samsungtv/entity.py @@ -106,5 +106,7 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity) self.entity_id, ) raise HomeAssistantError( - f"Entity {self.entity_id} does not support this service." + translation_domain=DOMAIN, + translation_key="service_unsupported", + translation_placeholders={"entity": self.entity_id}, ) diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index c9d08f756d0..65ed7a3fc23 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -47,5 +47,13 @@ "trigger_type": { "samsungtv.turn_on": "Device is requested to turn on" } + }, + "exceptions": { + "unhandled_trigger_type": { + "message": "Unhandled trigger type {trigger_type}." + }, + "service_unsupported": { + "message": "Entity {entity} does not support this action." + } } } diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index 854c92207bf..da7871ca9c5 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -122,9 +122,14 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None: """Test turn on.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) - with pytest.raises(HomeAssistantError, match="does not support this service"): + 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 exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == "service_unsupported" + assert exc_info.value.translation_placeholders == { + "entity": ENTITY_ID, + } From fcb8cdc146ebaefedd9374e79d2d8de3cc4047f9 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 9 Apr 2025 16:44:36 +0200 Subject: [PATCH 187/258] Add missing strings to Fritz (#142413) * Add missing strings to Fritz * update quality scale * add common section this avoids later re-structuring and re-translating * fix strings * fix strings * apply review comment --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/fritz/quality_scale.yaml | 4 +- homeassistant/components/fritz/strings.json | 40 ++++++++++++++++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fritz/quality_scale.yaml b/homeassistant/components/fritz/quality_scale.yaml index 805705eb4b4..361d61df6a0 100644 --- a/homeassistant/components/fritz/quality_scale.yaml +++ b/homeassistant/components/fritz/quality_scale.yaml @@ -7,9 +7,7 @@ rules: config-flow-test-coverage: status: todo comment: one coverage miss in line 110 - config-flow: - status: todo - comment: data_description are missing + config-flow: done dependency-transparency: done docs-actions: done docs-high-level-description: done diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 06a07cba79e..6191fc524dd 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -1,4 +1,11 @@ { + "common": { + "data_description_host": "The hostname or IP address of your FRITZ!Box router.", + "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." + }, "config": { "flow_title": "{name}", "step": { @@ -9,6 +16,11 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "ssl": "[%key:common::config_flow::data::ssl%]" + }, + "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%]" } }, "reauth_confirm": { @@ -17,6 +29,10 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "[%key:component::fritz::common::data_description_username%]", + "password": "[%key:component::fritz::common::data_description_password%]" } }, "reconfigure": { @@ -28,8 +44,9 @@ "ssl": "[%key:common::config_flow::data::ssl%]" }, "data_description": { - "host": "The hostname or IP address of your FRITZ!Box router.", - "port": "Leave it empty to use the default port." + "host": "[%key:component::fritz::common::data_description_host%]", + "port": "[%key:component::fritz::common::data_description_port%]", + "ssl": "[%key:component::fritz::common::data_description_ssl%]" } }, "user": { @@ -43,8 +60,11 @@ "ssl": "[%key:common::config_flow::data::ssl%]" }, "data_description": { - "host": "The hostname or IP address of your FRITZ!Box router.", - "port": "Leave it empty to use the default port." + "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%]" } } }, @@ -70,6 +90,10 @@ "data": { "consider_home": "Seconds to consider a device at 'home'", "old_discovery": "Enable old discovery method" + }, + "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." } } } @@ -169,8 +193,12 @@ "config_entry_not_found": { "message": "Failed to perform action \"{service}\". Config entry for target not found" }, - "service_parameter_unknown": { "message": "Action or parameter unknown" }, - "service_not_supported": { "message": "Action not supported" }, + "service_parameter_unknown": { + "message": "Action or parameter unknown" + }, + "service_not_supported": { + "message": "Action not supported" + }, "error_refresh_hosts_info": { "message": "Error refreshing hosts info" }, From 36ec857523ccc14e43dbb08b03cc1f583a07d6af Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:26:58 +0200 Subject: [PATCH 188/258] Fix reload of AVM FRITZ!Tools when new connected device is detected (#142430) --- homeassistant/components/fritz/button.py | 12 +----------- homeassistant/components/fritz/coordinator.py | 12 +++++++++++- homeassistant/components/fritz/entity.py | 3 +++ homeassistant/components/fritz/switch.py | 10 ---------- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/fritz/button.py b/homeassistant/components/fritz/button.py index 74e8ab5e43e..ddc0be3a6d9 100644 --- a/homeassistant/components/fritz/button.py +++ b/homeassistant/components/fritz/button.py @@ -18,7 +18,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, Device from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, DOMAIN, MeshRoles +from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, MeshRoles from .coordinator import ( FRITZ_DATA_KEY, AvmWrapper, @@ -175,16 +175,6 @@ class FritzBoxWOLButton(FritzDeviceBase, ButtonEntity): self._name = f"{self.hostname} Wake on LAN" self._attr_unique_id = f"{self._mac}_wake_on_lan" self._is_available = True - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, self._mac)}, - default_manufacturer="AVM", - default_model="FRITZ!Box Tracked device", - default_name=device.hostname, - via_device=( - DOMAIN, - avm_wrapper.unique_id, - ), - ) async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index d60232ec8ad..c0121ed9aa1 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -526,7 +526,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): def manage_device_info( self, dev_info: Device, dev_mac: str, consider_home: bool ) -> bool: - """Update device lists.""" + """Update device lists and return if device is new.""" _LOGGER.debug("Client dev_info: %s", dev_info) if dev_mac in self._devices: @@ -536,6 +536,16 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): device = FritzDevice(dev_mac, dev_info.name) device.update(dev_info, consider_home) self._devices[dev_mac] = device + + # manually register device entry for new connected device + dr.async_get(self.hass).async_get_or_create( + config_entry_id=self.config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, dev_mac)}, + default_manufacturer="AVM", + default_model="FRITZ!Box Tracked device", + default_name=device.hostname, + via_device=(DOMAIN, self.unique_id), + ) return True async def async_send_signal_device_update(self, new_device: bool) -> None: diff --git a/homeassistant/components/fritz/entity.py b/homeassistant/components/fritz/entity.py index 33eb60d72cf..e8b5c49fd43 100644 --- a/homeassistant/components/fritz/entity.py +++ b/homeassistant/components/fritz/entity.py @@ -26,6 +26,9 @@ class FritzDeviceBase(CoordinatorEntity[AvmWrapper]): self._avm_wrapper = avm_wrapper self._mac: str = device.mac_address self._name: str = device.hostname or DEFAULT_DEVICE_NAME + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, device.mac_address)} + ) @property def name(self) -> str: diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 8b4816f7451..b627e0150a9 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -511,16 +511,6 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): self._name = f"{device.hostname} Internet Access" self._attr_unique_id = f"{self._mac}_internet_access" self._attr_entity_category = EntityCategory.CONFIG - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, self._mac)}, - default_manufacturer="AVM", - default_model="FRITZ!Box Tracked device", - default_name=device.hostname, - via_device=( - DOMAIN, - avm_wrapper.unique_id, - ), - ) @property def is_on(self) -> bool | None: From d39fa39a035ef09d7c5a448363a4ca35d6fc8cb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Apr 2025 22:11:15 -1000 Subject: [PATCH 189/258] Fix HKC showing hvac_action as idle when fan is active and heat cool target is off (#142443) * Fix HKC showing hvac_action as idle when fan is active and heat cool target is off fixes #142442 * comment relocation --- .../components/homekit_controller/climate.py | 12 +- .../fixtures/ecobee3_lite.json | 3436 ++++++++++++++++ .../snapshots/test_init.ambr | 3458 +++++++++++++++++ .../homekit_controller/test_climate.py | 23 +- 4 files changed, 6921 insertions(+), 8 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/ecobee3_lite.json diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 7341bbd3a4a..4c8bf8517be 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -659,13 +659,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity): # e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit. # Can be 0 - 2 (Off, Heat, Cool) - # If the HVAC is switched off, it must be idle - # This works around a bug in some devices (like Eve radiator valves) that - # return they are heating when they are not. target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - if target == HeatingCoolingTargetValues.OFF: - return HVACAction.IDLE - value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT) current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value) @@ -679,6 +673,12 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity): ): return HVACAction.FAN + # If the HVAC is switched off, it must be idle + # This works around a bug in some devices (like Eve radiator valves) that + # return they are heating when they are not. + if target == HeatingCoolingTargetValues.OFF: + return HVACAction.IDLE + return current_hass_value @property diff --git a/tests/components/homekit_controller/fixtures/ecobee3_lite.json b/tests/components/homekit_controller/fixtures/ecobee3_lite.json new file mode 100644 index 00000000000..0656ed20fdb --- /dev/null +++ b/tests/components/homekit_controller/fixtures/ecobee3_lite.json @@ -0,0 +1,3436 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "ecobee3 lite", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Thermostat", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.8.70226", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 11, + "perms": ["pr", "hd"], + "format": "string", + "value": "4.1;3fac0fb4", + "maxLen": 64 + }, + { + "type": "00000220-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "hd"], + "format": "data", + "value": "u4qz9YgSXzQ=" + }, + { + "type": "000000A6-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": ["pr", "ev"], + "format": "uint32", + "value": 0, + "description": "Accessory Flags" + } + ] + }, + { + "iid": 30, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 31, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 16, + "type": "0000004A-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000000F-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Current Heating Cooling State", + "minValue": 0, + "maxValue": 2, + "minStep": 1, + "valid-values": [0, 1, 2] + }, + { + "type": "00000033-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 0, + "description": "Target Heating Cooling State", + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "valid-values": [0, 1, 2, 3] + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr", "ev"], + "format": "float", + "value": 21.2, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 40.0, + "minStep": 0.1 + }, + { + "type": "00000035-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 22.2, + "description": "Target Temperature", + "unit": "celsius", + "minValue": 7.2, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "00000036-0000-1000-8000-0026BB765291", + "iid": 21, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Temperature Display Units", + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "valid-values": [0, 1] + }, + { + "type": "0000000D-0000-1000-8000-0026BB765291", + "iid": 22, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 25.0, + "description": "Cooling Threshold Temperature", + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "00000012-0000-1000-8000-0026BB765291", + "iid": 23, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 22.2, + "description": "Heating Threshold Temperature", + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 24, + "perms": ["pr", "ev"], + "format": "float", + "value": 45.0, + "description": "Current Relative Humidity", + "unit": "percentage", + "minValue": 0, + "maxValue": 100.0, + "minStep": 1.0 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 27, + "perms": ["pr"], + "format": "string", + "value": "Thermostat", + "description": "Name", + "maxLen": 64 + }, + { + "type": "000000BF-0000-1000-8000-0026BB765291", + "iid": 75, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 0, + "description": "Target Fan State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "000000AF-0000-1000-8000-0026BB765291", + "iid": 76, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Current Fan State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "B7DDB9A3-54BB-4572-91D2-F1F5B0510F8C", + "iid": 33, + "perms": ["pr"], + "format": "uint8", + "value": 3, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "E4489BBC-5227-4569-93E5-B345E3E5508F", + "iid": 34, + "perms": ["pr", "pw"], + "format": "float", + "value": 22.2, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "7D381BAA-20F9-40E5-9BE9-AEB92D4BECEF", + "iid": 35, + "perms": ["pr", "pw"], + "format": "float", + "value": 25.0, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "73AAB542-892A-4439-879A-D2A883724B69", + "iid": 36, + "perms": ["pr", "pw"], + "format": "float", + "value": 17.8, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "5DA985F0-898A-4850-B987-B76C6C78D670", + "iid": 37, + "perms": ["pr", "pw"], + "format": "float", + "value": 25.6, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "05B97374-6DC0-439B-A0FA-CA33F612D425", + "iid": 38, + "perms": ["pr", "pw"], + "format": "float", + "value": 20.0, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "A251F6E7-AC46-4190-9C5D-3D06277BDF9F", + "iid": 39, + "perms": ["pr", "pw"], + "format": "float", + "value": 24.4, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "1B300BC2-CFFC-47FF-89F9-BD6CCF5F2853", + "iid": 40, + "perms": ["pw"], + "format": "uint8", + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "1621F556-1367-443C-AF19-82AF018E99DE", + "iid": 41, + "perms": ["pr", "pw"], + "format": "string", + "value": "2025-04-06T23:30:00-05:00R", + "maxLen": 64 + }, + { + "type": "FA128DE6-9D7D-49A4-B6D8-4E4E234DEE38", + "iid": 48, + "perms": ["pw"], + "format": "bool" + }, + { + "type": "4A6AE4F6-036C-495D-87CC-B3702B437741", + "iid": 49, + "perms": ["pr"], + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 4, + "minStep": 1 + }, + { + "type": "DB7BF261-7042-4194-8BD1-3AA22830AEDD", + "iid": 50, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "41935E3E-B54D-42E9-B8B9-D33C6319F0AF", + "iid": 51, + "perms": ["pr"], + "format": "bool", + "value": false + }, + { + "type": "C35DA3C0-E004-40E3-B153-46655CDD9214", + "iid": 52, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 100, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "48F62AEC-4171-4B4A-8F0E-1EEB6708B3FB", + "iid": 53, + "perms": ["pr"], + "format": "uint8", + "value": 100, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "1B1515F2-CC45-409F-991F-C480987F92C3", + "iid": 54, + "perms": ["pr"], + "format": "string", + "value": "The Hive is humming along. You have no pending alerts or reminders.", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 4295608971, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Master BR", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBRSE4", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Master BR", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 208, + "type": "0000008A-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 209, + "perms": ["pr", "ev"], + "format": "float", + "value": 22.4, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 100.0, + "minStep": 0.1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 210, + "perms": ["pr"], + "format": "string", + "value": "Master BR Temperature", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 211, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 212, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 1, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Master BR Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 691, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Master BR Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 691, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + } + ] + }, + { + "aid": 4295608960, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Basement", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBRSE4", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Basement", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 208, + "type": "0000008A-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 209, + "perms": ["pr", "ev"], + "format": "float", + "value": 20.3, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 100.0, + "minStep": 0.1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 210, + "perms": ["pr"], + "format": "string", + "value": "Basement Temperature", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 211, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 212, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Basement Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 9158, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Basement Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 9158, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + } + ] + }, + { + "aid": 4295016858, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Living Room", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBRSE4", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Living Room", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 208, + "type": "0000008A-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 209, + "perms": ["pr", "ev"], + "format": "float", + "value": 21.0, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 100.0, + "minStep": 0.1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 210, + "perms": ["pr"], + "format": "string", + "value": "Living Room Temperature", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 211, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 212, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Living Room Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": -1, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Living Room Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": -1, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + } + ] + }, + { + "aid": 4295016969, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBRSE4", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 208, + "type": "0000008A-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 209, + "perms": ["pr", "ev"], + "format": "float", + "value": 21.6, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 100.0, + "minStep": 0.1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 210, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR Temperature", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 211, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 212, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": -1, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": -1, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + } + ] + }, + { + "aid": 4298584118, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Master BR Window", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBDWC01", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Master BR Window", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Master BR Window Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 1421, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Master BR Window Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 821, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 224, + "type": "00000080-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000006A-0000-1000-8000-0026BB765291", + "iid": 225, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Contact Sensor State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 226, + "perms": ["pr"], + "format": "string", + "value": "Master BR Window Contact", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 4298649931, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Loft window", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBDWC01", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Loft window", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Loft window Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 327, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Loft window Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 328, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 224, + "type": "00000080-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000006A-0000-1000-8000-0026BB765291", + "iid": 225, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Contact Sensor State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 226, + "perms": ["pr"], + "format": "string", + "value": "Loft window Contact", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 4298527970, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Front Door", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBDWC01", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Front Door", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 1, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Front Door Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 1473, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Front Door Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 873, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 224, + "type": "00000080-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000006A-0000-1000-8000-0026BB765291", + "iid": 225, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Contact Sensor State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 226, + "perms": ["pr"], + "format": "string", + "value": "Front Door Contact", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 4298527962, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Garage Door", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBDWC01", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Garage Door", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 1, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Garage Door Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 1189, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Garage Door Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 888, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 224, + "type": "00000080-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000006A-0000-1000-8000-0026BB765291", + "iid": 225, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Contact Sensor State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 226, + "perms": ["pr"], + "format": "string", + "value": "Garage Door Contact", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 4298360914, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Basement Window 1", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBDWC01", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Basement Window 1", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Basement Window 1 Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": -1, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Basement Window 1 Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": -1, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 224, + "type": "00000080-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000006A-0000-1000-8000-0026BB765291", + "iid": 225, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Contact Sensor State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 226, + "perms": ["pr"], + "format": "string", + "value": "Basement Window 1 Contact", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 4298360921, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Deck Door", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBDWC01", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Deck Door", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Deck Door Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 944, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Deck Door Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 884, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 224, + "type": "00000080-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000006A-0000-1000-8000-0026BB765291", + "iid": 225, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Contact Sensor State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 226, + "perms": ["pr"], + "format": "string", + "value": "Deck Door Contact", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 4298360712, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Living Room Window 1", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBDWC01", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Living Room Window 1", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 1, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Living Room Window 1 Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 1923, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Living Room Window 1 Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 625, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 224, + "type": "00000080-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000006A-0000-1000-8000-0026BB765291", + "iid": 225, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Contact Sensor State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 226, + "perms": ["pr"], + "format": "string", + "value": "Living Room Window 1 Contact", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 4298568508, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR Window", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "**REDACTED**", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "EBDWC01", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + } + ] + }, + { + "iid": 192, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 193, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "description": "Charging State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 194, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 100, + "description": "Battery Level", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 195, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 196, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR Window", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR Window Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr"], + "format": "int", + "value": 9060, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": false, + "description": "Motion Detected" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR Window Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 101, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 100, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr"], + "format": "int", + "value": 9060, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 224, + "type": "00000080-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000006A-0000-1000-8000-0026BB765291", + "iid": 225, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Contact Sensor State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 226, + "perms": ["pr"], + "format": "string", + "value": "Upstairs BR Window Contact", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 62b53df33f2..3bb9eb48106 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -4195,6 +4195,3464 @@ }), ]) # --- +# name: test_snapshots[ecobee3_lite] + list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4295608960', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBRSE4', + 'model_id': None, + 'name': 'Basement', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.basement_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608960_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Basement Motion', + }), + 'entity_id': 'binary_sensor.basement_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.basement_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608960_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Basement Occupancy', + }), + 'entity_id': 'binary_sensor.basement_occupancy', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.basement_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608960_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Basement Identify', + }), + 'entity_id': 'button.basement_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.basement_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Basement Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608960_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Basement Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.basement_battery', + 'state': '100', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.basement_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Temperature', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608960_208', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'temperature', + 'friendly_name': 'Basement Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'entity_id': 'sensor.basement_temperature', + 'state': '20.3', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4298360914', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBDWC01', + 'model_id': None, + 'name': 'Basement Window 1', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.basement_window_1_contact', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Window 1 Contact', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360914_224', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'opening', + 'friendly_name': 'Basement Window 1 Contact', + }), + 'entity_id': 'binary_sensor.basement_window_1_contact', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.basement_window_1_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Window 1 Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360914_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Basement Window 1 Motion', + }), + 'entity_id': 'binary_sensor.basement_window_1_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.basement_window_1_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Window 1 Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360914_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Basement Window 1 Occupancy', + }), + 'entity_id': 'binary_sensor.basement_window_1_occupancy', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.basement_window_1_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Window 1 Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360914_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Basement Window 1 Identify', + }), + 'entity_id': 'button.basement_window_1_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.basement_window_1_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Basement Window 1 Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360914_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Basement Window 1 Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.basement_window_1_battery', + 'state': '100', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4298360921', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBDWC01', + 'model_id': None, + 'name': 'Deck Door', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.deck_door_contact', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Deck Door Contact', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360921_224', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'opening', + 'friendly_name': 'Deck Door Contact', + }), + 'entity_id': 'binary_sensor.deck_door_contact', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.deck_door_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Deck Door Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360921_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Deck Door Motion', + }), + 'entity_id': 'binary_sensor.deck_door_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.deck_door_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Deck Door Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360921_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Deck Door Occupancy', + }), + 'entity_id': 'binary_sensor.deck_door_occupancy', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.deck_door_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Deck Door Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360921_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Deck Door Identify', + }), + 'entity_id': 'button.deck_door_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.deck_door_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Deck Door Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360921_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Deck Door Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.deck_door_battery', + 'state': '100', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4298527970', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBDWC01', + 'model_id': None, + 'name': 'Front Door', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.front_door_contact', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Front Door Contact', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527970_224', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'opening', + 'friendly_name': 'Front Door Contact', + }), + 'entity_id': 'binary_sensor.front_door_contact', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.front_door_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Front Door Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527970_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Front Door Motion', + }), + 'entity_id': 'binary_sensor.front_door_motion', + 'state': 'on', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.front_door_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Front Door Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527970_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Front Door Occupancy', + }), + 'entity_id': 'binary_sensor.front_door_occupancy', + 'state': 'on', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.front_door_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Front Door Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527970_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Front Door Identify', + }), + 'entity_id': 'button.front_door_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.front_door_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Front Door Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527970_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Front Door Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.front_door_battery', + 'state': '100', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4298527962', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBDWC01', + 'model_id': None, + 'name': 'Garage Door', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.garage_door_contact', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Garage Door Contact', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527962_224', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'opening', + 'friendly_name': 'Garage Door Contact', + }), + 'entity_id': 'binary_sensor.garage_door_contact', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.garage_door_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Garage Door Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527962_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Garage Door Motion', + }), + 'entity_id': 'binary_sensor.garage_door_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.garage_door_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Garage Door Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527962_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Garage Door Occupancy', + }), + 'entity_id': 'binary_sensor.garage_door_occupancy', + 'state': 'on', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.garage_door_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Garage Door Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527962_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Garage Door Identify', + }), + 'entity_id': 'button.garage_door_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_door_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Garage Door Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298527962_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Garage Door Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.garage_door_battery', + 'state': '100', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4295016858', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBRSE4', + 'model_id': None, + 'name': 'Living Room', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.living_room_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Living Room Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016858_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Living Room Motion', + }), + 'entity_id': 'binary_sensor.living_room_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.living_room_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Living Room Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016858_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Living Room Occupancy', + }), + 'entity_id': 'binary_sensor.living_room_occupancy', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.living_room_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Living Room Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016858_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Living Room Identify', + }), + 'entity_id': 'button.living_room_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.living_room_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Living Room Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016858_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Living Room Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.living_room_battery', + 'state': '100', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.living_room_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Living Room Temperature', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016858_208', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'temperature', + 'friendly_name': 'Living Room Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'entity_id': 'sensor.living_room_temperature', + 'state': '21.0', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4298360712', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBDWC01', + 'model_id': None, + 'name': 'Living Room Window 1', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.living_room_window_1_contact', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Living Room Window 1 Contact', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360712_224', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'opening', + 'friendly_name': 'Living Room Window 1 Contact', + }), + 'entity_id': 'binary_sensor.living_room_window_1_contact', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.living_room_window_1_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Living Room Window 1 Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360712_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Living Room Window 1 Motion', + }), + 'entity_id': 'binary_sensor.living_room_window_1_motion', + 'state': 'on', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.living_room_window_1_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Living Room Window 1 Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360712_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Living Room Window 1 Occupancy', + }), + 'entity_id': 'binary_sensor.living_room_window_1_occupancy', + 'state': 'on', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.living_room_window_1_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Living Room Window 1 Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360712_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Living Room Window 1 Identify', + }), + 'entity_id': 'button.living_room_window_1_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.living_room_window_1_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Living Room Window 1 Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298360712_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Living Room Window 1 Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.living_room_window_1_battery', + 'state': '100', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4298649931', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBDWC01', + 'model_id': None, + 'name': 'Loft window', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.loft_window_contact', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Loft window Contact', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298649931_224', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'opening', + 'friendly_name': 'Loft window Contact', + }), + 'entity_id': 'binary_sensor.loft_window_contact', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.loft_window_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Loft window Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298649931_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Loft window Motion', + }), + 'entity_id': 'binary_sensor.loft_window_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.loft_window_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Loft window Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298649931_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Loft window Occupancy', + }), + 'entity_id': 'binary_sensor.loft_window_occupancy', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.loft_window_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Loft window Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298649931_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Loft window Identify', + }), + 'entity_id': 'button.loft_window_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.loft_window_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Loft window Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298649931_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Loft window Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.loft_window_battery', + 'state': '100', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4295608971', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBRSE4', + 'model_id': None, + 'name': 'Master BR', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.master_br_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Master BR Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608971_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Master BR Motion', + }), + 'entity_id': 'binary_sensor.master_br_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.master_br_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Master BR Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608971_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Master BR Occupancy', + }), + 'entity_id': 'binary_sensor.master_br_occupancy', + 'state': 'on', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.master_br_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Master BR Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608971_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Master BR Identify', + }), + 'entity_id': 'button.master_br_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.master_br_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Master BR Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608971_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Master BR Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.master_br_battery', + 'state': '100', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.master_br_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Master BR Temperature', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295608971_208', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'temperature', + 'friendly_name': 'Master BR Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'entity_id': 'sensor.master_br_temperature', + 'state': '22.4', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4298584118', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBDWC01', + 'model_id': None, + 'name': 'Master BR Window', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.master_br_window_contact', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Master BR Window Contact', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298584118_224', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'opening', + 'friendly_name': 'Master BR Window Contact', + }), + 'entity_id': 'binary_sensor.master_br_window_contact', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.master_br_window_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Master BR Window Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298584118_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Master BR Window Motion', + }), + 'entity_id': 'binary_sensor.master_br_window_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.master_br_window_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Master BR Window Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298584118_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Master BR Window Occupancy', + }), + 'entity_id': 'binary_sensor.master_br_window_occupancy', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.master_br_window_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Master BR Window Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298584118_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Master BR Window Identify', + }), + 'entity_id': 'button.master_br_window_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.master_br_window_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Master BR Window Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298584118_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Master BR Window Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.master_br_window_battery', + 'state': '100', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:1', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'ecobee3 lite', + 'model_id': None, + 'name': 'Thermostat', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '4.8.70226', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.thermostat_clear_hold', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Thermostat Clear Hold', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_16_48', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'Thermostat Clear Hold', + }), + 'entity_id': 'button.thermostat_clear_hold', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.thermostat_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Thermostat Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Thermostat Identify', + }), + 'entity_id': 'button.thermostat_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'on', + 'auto', + ]), + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Thermostat', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_16', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'current_humidity': 45.0, + 'current_temperature': 21.2, + 'fan_mode': 'on', + 'fan_modes': list([ + 'on', + 'auto', + ]), + 'friendly_name': 'Thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': None, + }), + 'entity_id': 'climate.thermostat', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'home', + 'sleep', + 'away', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.thermostat_current_mode', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Thermostat Current Mode', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ecobee_mode', + 'unique_id': '00:00:00:00:00:00_1_16_33', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'Thermostat Current Mode', + 'options': list([ + 'home', + 'sleep', + 'away', + ]), + }), + 'entity_id': 'select.thermostat_current_mode', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'celsius', + 'fahrenheit', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.thermostat_temperature_display_units', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Thermostat Temperature Display Units', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_display_units', + 'unique_id': '00:00:00:00:00:00_1_16_21', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'Thermostat Temperature Display Units', + 'options': list([ + 'celsius', + 'fahrenheit', + ]), + }), + 'entity_id': 'select.thermostat_temperature_display_units', + 'state': 'fahrenheit', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.thermostat_current_humidity', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Thermostat Current Humidity', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_16_24', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'humidity', + 'friendly_name': 'Thermostat Current Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.thermostat_current_humidity', + 'state': '45.0', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.thermostat_current_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Thermostat Current Temperature', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_16_19', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'temperature', + 'friendly_name': 'Thermostat Current Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'entity_id': 'sensor.thermostat_current_temperature', + 'state': '21.2', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4295016969', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBRSE4', + 'model_id': None, + 'name': 'Upstairs BR', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.upstairs_br_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upstairs BR Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016969_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Upstairs BR Motion', + }), + 'entity_id': 'binary_sensor.upstairs_br_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.upstairs_br_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upstairs BR Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016969_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Upstairs BR Occupancy', + }), + 'entity_id': 'binary_sensor.upstairs_br_occupancy', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.upstairs_br_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upstairs BR Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016969_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Upstairs BR Identify', + }), + 'entity_id': 'button.upstairs_br_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.upstairs_br_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Upstairs BR Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016969_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Upstairs BR Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.upstairs_br_battery', + 'state': '100', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.upstairs_br_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upstairs BR Temperature', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4295016969_208', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'temperature', + 'friendly_name': 'Upstairs BR Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'entity_id': 'sensor.upstairs_br_temperature', + 'state': '21.6', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'config_entries_subentries': dict({ + 'TestData': list([ + None, + ]), + }), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:4298568508', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'ecobee Inc.', + 'model': 'EBDWC01', + 'model_id': None, + 'name': 'Upstairs BR Window', + 'name_by_user': None, + 'primary_config_entry': 'TestData', + 'serial_number': '**REDACTED**', + 'suggested_area': None, + 'sw_version': '1.0.0', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.upstairs_br_window_contact', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upstairs BR Window Contact', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298568508_224', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'opening', + 'friendly_name': 'Upstairs BR Window Contact', + }), + 'entity_id': 'binary_sensor.upstairs_br_window_contact', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.upstairs_br_window_motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upstairs BR Window Motion', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298568508_56', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'Upstairs BR Window Motion', + }), + 'entity_id': 'binary_sensor.upstairs_br_window_motion', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.upstairs_br_window_occupancy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upstairs BR Window Occupancy', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298568508_57', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'occupancy', + 'friendly_name': 'Upstairs BR Window Occupancy', + }), + 'entity_id': 'binary_sensor.upstairs_br_window_occupancy', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.upstairs_br_window_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upstairs BR Window Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298568508_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Upstairs BR Window Identify', + }), + 'entity_id': 'button.upstairs_br_window_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'config_subentry_id': None, + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.upstairs_br_window_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Upstairs BR Window Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4298568508_192', + 'unit_of_measurement': '%', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'Upstairs BR Window Battery', + 'icon': 'mdi:battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'entity_id': 'sensor.upstairs_br_window_battery', + 'state': '100', + }), + }), + ]), + }), + ]) +# --- # name: test_snapshots[ecobee3_no_sensors] list([ dict({ diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 62c73af9977..b119b5f7b80 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -8,6 +8,8 @@ from aiohomekit.model.characteristics import ( CharacteristicsTypes, CurrentFanStateValues, CurrentHeaterCoolerStateValues, + HeatingCoolingCurrentValues, + HeatingCoolingTargetValues, SwingModeValues, TargetHeaterCoolerStateValues, ) @@ -20,6 +22,7 @@ from homeassistant.components.climate import ( SERVICE_SET_HVAC_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + HVACAction, HVACMode, ) from homeassistant.core import HomeAssistant @@ -662,7 +665,7 @@ async def test_hvac_mode_vs_hvac_action( state = await helper.poll_and_get_state() assert state.state == "heat" - assert state.attributes["hvac_action"] == "fan" + assert state.attributes["hvac_action"] == HVACAction.FAN # Simulate that current temperature is below target temp # Heating might be on and hvac_action currently 'heat' @@ -676,7 +679,23 @@ async def test_hvac_mode_vs_hvac_action( state = await helper.poll_and_get_state() assert state.state == "heat" - assert state.attributes["hvac_action"] == "heating" + assert state.attributes["hvac_action"] == HVACAction.HEATING + + # If the fan is active, and the heating is off, the hvac_action should be 'fan' + # and not 'idle' or 'heating' + await helper.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.FAN_STATE_CURRENT: CurrentFanStateValues.ACTIVE, + CharacteristicsTypes.HEATING_COOLING_CURRENT: HeatingCoolingCurrentValues.IDLE, + CharacteristicsTypes.HEATING_COOLING_TARGET: HeatingCoolingTargetValues.OFF, + CharacteristicsTypes.FAN_STATE_CURRENT: CurrentFanStateValues.ACTIVE, + }, + ) + + state = await helper.poll_and_get_state() + assert state.state == HVACMode.OFF + assert state.attributes["hvac_action"] == HVACAction.FAN async def test_hvac_mode_vs_hvac_action_current_mode_wrong( From 44a92ca81c2fc2b55fbfd673527c98778d224972 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 7 Apr 2025 09:35:44 +0200 Subject: [PATCH 190/258] Fix Reolink smart AI sensors (#142454) --- homeassistant/components/reolink/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index 39910bbc52a..95c5f1982c3 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -301,7 +301,7 @@ async def async_setup_entry( ) for entity_description in BINARY_SMART_AI_SENSORS for location in api.baichuan.smart_location_list( - channel, entity_description.key + channel, entity_description.smart_type ) if entity_description.supported(api, channel, location) ) From d59200a9f59bcb6a58664855ad5d99b3e17537d4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:56:21 +0200 Subject: [PATCH 191/258] Fix kelvin parameter in light action specifications (#142456) --- homeassistant/components/light/services.yaml | 4 ++-- homeassistant/components/light/strings.json | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 2a1fbd11afd..77c5df88dd3 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -199,7 +199,7 @@ turn_on: example: "[255, 100, 100]" selector: color_rgb: - kelvin: &kelvin + color_temp_kelvin: &color_temp_kelvin filter: *color_temp_support selector: color_temp: @@ -317,7 +317,7 @@ toggle: fields: transition: *transition rgb_color: *rgb_color - kelvin: *kelvin + color_temp_kelvin: *color_temp_kelvin brightness_pct: *brightness_pct effect: *effect advanced_fields: diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 4a3b98ded46..ecacb3c2daa 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -19,8 +19,8 @@ "field_flash_name": "Flash", "field_hs_color_description": "Color in hue/sat format. A list of two integers. Hue is 0-360 and Sat is 0-100.", "field_hs_color_name": "Hue/Sat color", - "field_kelvin_description": "Color temperature in Kelvin.", - "field_kelvin_name": "Color temperature", + "field_color_temp_kelvin_description": "Color temperature in Kelvin.", + "field_color_temp_kelvin_name": "Color temperature", "field_profile_description": "Name of a light profile to use.", "field_profile_name": "Profile", "field_rgb_color_description": "The color in RGB format. A list of three integers between 0 and 255 representing the values of red, green, and blue.", @@ -322,9 +322,9 @@ "name": "[%key:component::light::common::field_color_temp_name%]", "description": "[%key:component::light::common::field_color_temp_description%]" }, - "kelvin": { - "name": "[%key:component::light::common::field_kelvin_name%]", - "description": "[%key:component::light::common::field_kelvin_description%]" + "color_temp_kelvin": { + "name": "[%key:component::light::common::field_color_temp_kelvin_name%]", + "description": "[%key:component::light::common::field_color_temp_kelvin_description%]" }, "brightness": { "name": "[%key:component::light::common::field_brightness_name%]", @@ -420,9 +420,9 @@ "name": "[%key:component::light::common::field_color_temp_name%]", "description": "[%key:component::light::common::field_color_temp_description%]" }, - "kelvin": { - "name": "[%key:component::light::common::field_kelvin_name%]", - "description": "[%key:component::light::common::field_kelvin_description%]" + "color_temp_kelvin": { + "name": "[%key:component::light::common::field_color_temp_kelvin_name%]", + "description": "[%key:component::light::common::field_color_temp_kelvin_description%]" }, "brightness": { "name": "[%key:component::light::common::field_brightness_name%]", From 485b28d9eaf2e91eaf8b82c43f4206b414d46804 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Apr 2025 08:58:08 +0200 Subject: [PATCH 192/258] Bump aioshelly to version 13.4.1 (#142477) * Bymp aioshelly to 13.4.1 * Catch InvalidHostError --------- Co-authored-by: J. Nick Koston --- homeassistant/components/shelly/config_flow.py | 3 +++ homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/strings.json | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/test_config_flow.py | 2 ++ 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 200a88ea24c..6e41df282ef 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -12,6 +12,7 @@ from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, InvalidAuthError, + InvalidHostError, MacAddressMismatchError, ) from aioshelly.rpc_device import RpcDevice @@ -157,6 +158,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self.info = await self._async_get_info(host, port) except DeviceConnectionError: errors["base"] = "cannot_connect" + except InvalidHostError: + errors["base"] = "invalid_host" except Exception: # noqa: BLE001 LOGGER.exception("Unexpected exception") errors["base"] = "unknown" diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index e863720e476..19ccd1354a7 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["aioshelly"], - "requirements": ["aioshelly==13.4.0"], + "requirements": ["aioshelly==13.4.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index afc3f92a3ce..8c8c1b94c48 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -51,6 +51,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", "unknown": "[%key:common::config_flow::error::unknown%]", "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support", "custom_port_not_supported": "Gen1 device does not support custom port.", diff --git a/requirements_all.txt b/requirements_all.txt index 69d03f97152..d835408a0a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.4.0 +aioshelly==13.4.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be52eef360c..752e9d08a61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -353,7 +353,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.4.0 +aioshelly==13.4.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index fffffc21cae..60883ebf5bd 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -11,6 +11,7 @@ from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, InvalidAuthError, + InvalidHostError, ) import pytest @@ -308,6 +309,7 @@ async def test_form_auth( ("exc", "base_error"), [ (DeviceConnectionError, "cannot_connect"), + (InvalidHostError, "invalid_host"), (ValueError, "unknown"), ], ) From 9410061405073b3abd024f32e8ef62bb93cf3164 Mon Sep 17 00:00:00 2001 From: Wilfred Ketelaar Date: Mon, 7 Apr 2025 15:54:08 +0200 Subject: [PATCH 193/258] Fixed Renault charge state icon (#142478) Fixed charge state icon (duplicate mdi prefix) --- homeassistant/components/renault/icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/renault/icons.json b/homeassistant/components/renault/icons.json index 8b9c4885eaa..aa9175052fb 100644 --- a/homeassistant/components/renault/icons.json +++ b/homeassistant/components/renault/icons.json @@ -35,7 +35,7 @@ }, "sensor": { "charge_state": { - "default": "mdi:mdi:flash-off", + "default": "mdi:flash-off", "state": { "charge_in_progress": "mdi:flash" } From f702f3efcdc0ac2da9dc7017083ed95a20e028ff Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 9 Apr 2025 08:53:44 +0200 Subject: [PATCH 194/258] Fix Shelly initialization if device runs large script (#142487) * Don't check the whole script to see if it generates events * Fix tests --------- Co-authored-by: Shay Levy --- homeassistant/components/shelly/const.py | 4 ++++ homeassistant/components/shelly/utils.py | 3 ++- tests/components/shelly/conftest.py | 8 ++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 43fb6df18d0..0c64df52409 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -277,3 +277,7 @@ ROLE_TO_DEVICE_CLASS_MAP = { "current_humidity": SensorDeviceClass.HUMIDITY, "current_temperature": SensorDeviceClass.TEMPERATURE, } + +# 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 diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 474e2bb9410..a5e08faf0e0 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -58,6 +58,7 @@ from .const import ( GEN2_BETA_RELEASE_URL, GEN2_RELEASE_URL, LOGGER, + MAX_SCRIPT_SIZE, RPC_INPUTS_EVENTS_TYPES, SHAIR_MAX_WORK_HOURS, SHBTN_INPUTS_EVENTS_TYPES, @@ -642,7 +643,7 @@ def get_rpc_ws_url(hass: HomeAssistant) -> str | None: async def get_rpc_script_event_types(device: RpcDevice, id: int) -> list[str]: """Return a list of event types for a specific script.""" - code_response = await device.script_getcode(id) + code_response = await device.script_getcode(id, bytes_to_read=MAX_SCRIPT_SIZE) matches = SHELLY_EMIT_EVENT_PATTERN.finditer(code_response["data"]) return sorted([*{str(event_type.group(1)) for event_type in matches}]) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 8f8255235be..2a386a1628c 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -492,7 +492,9 @@ def _mock_rpc_device(version: str | None = None): initialized=True, connected=True, script_getcode=AsyncMock( - side_effect=lambda script_id: {"data": MOCK_SCRIPTS[script_id - 1]} + side_effect=lambda script_id, bytes_to_read: { + "data": MOCK_SCRIPTS[script_id - 1] + } ), xmod_info={}, ) @@ -514,7 +516,9 @@ def _mock_blu_rtv_device(version: str | None = None): initialized=True, connected=True, script_getcode=AsyncMock( - side_effect=lambda script_id: {"data": MOCK_SCRIPTS[script_id - 1]} + side_effect=lambda script_id, bytes_to_read: { + "data": MOCK_SCRIPTS[script_id - 1] + } ), xmod_info={}, ) From c540acf2bd40cd6c3925f869a57594cd2942f7fe Mon Sep 17 00:00:00 2001 From: Ivan Lopez Hernandez Date: Mon, 7 Apr 2025 21:20:54 -0700 Subject: [PATCH 195/258] Handle None on the response candidates in Google Generative AI (#142497) * Added type checking on the candidates list * Made error message a constant --- .../conversation.py | 14 +++++++-- .../test_conversation.py | 29 ++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 7c19c5445a7..73a82b98664 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -55,6 +55,10 @@ from .const import ( # Max number of back and forth with the LLM to generate a response MAX_TOOL_ITERATIONS = 10 +ERROR_GETTING_RESPONSE = ( + "Sorry, I had a problem getting a response from Google Generative AI." +) + async def async_setup_entry( hass: HomeAssistant, @@ -429,6 +433,12 @@ class GoogleGenerativeAIConversationEntity( 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) except ( APIError, @@ -452,9 +462,7 @@ class GoogleGenerativeAIConversationEntity( response_parts = chat_response.candidates[0].content.parts if not response_parts: - raise HomeAssistantError( - "Sorry, I had a problem getting a response from Google Generative AI." - ) + raise HomeAssistantError(ERROR_GETTING_RESPONSE) content = " ".join( [part.text.strip() for part in response_parts if part.text] ) diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 9c4ecc4f9a4..75cb308d5de 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components import conversation from homeassistant.components.conversation import UserContent, async_get_chat_log, trace from homeassistant.components.google_generative_ai_conversation.conversation import ( + ERROR_GETTING_RESPONSE, _escape_decode, _format_schema, ) @@ -492,7 +493,33 @@ async def test_empty_response( 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 getting a response from Google Generative AI." + ERROR_GETTING_RESPONSE + ) + + +@pytest.mark.usefixtures("mock_init_component") +async def test_none_response( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> 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", + ) + + 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 ) From d9f91598a59f8e3f9b830b06506e96f1bf33c622 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 8 Apr 2025 11:46:09 -0700 Subject: [PATCH 196/258] Fix range of Google Generative AI temperature (#142513) --- .../components/google_generative_ai_conversation/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py index ac6cb696a7d..ee980c9bf48 100644 --- a/homeassistant/components/google_generative_ai_conversation/config_flow.py +++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py @@ -303,7 +303,7 @@ async def google_generative_ai_config_option_schema( CONF_TEMPERATURE, description={"suggested_value": options.get(CONF_TEMPERATURE)}, default=RECOMMENDED_TEMPERATURE, - ): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)), + ): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)), vol.Optional( CONF_TOP_P, description={"suggested_value": options.get(CONF_TOP_P)}, From 8dc21ef619fed5359d81f75cd3cf5ce1a9e47d5d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Apr 2025 12:00:05 +0200 Subject: [PATCH 197/258] Allow max to be equal with min for mqtt number config validation (#142522) --- homeassistant/components/mqtt/number.py | 4 +- tests/components/mqtt/test_number.py | 59 ++++++++++++++++++------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 5ee93cfba07..c3cc31bf04f 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -70,8 +70,8 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( def validate_config(config: ConfigType) -> ConfigType: """Validate that the configuration is valid, throws if it isn't.""" - if config[CONF_MIN] >= config[CONF_MAX]: - raise vol.Invalid(f"'{CONF_MAX}' must be > '{CONF_MIN}'") + if config[CONF_MIN] > config[CONF_MAX]: + raise vol.Invalid(f"{CONF_MAX} must be >= {CONF_MIN}") return config diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index f391236aca4..fd54e5f0643 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -835,32 +835,57 @@ async def test_entity_debug_info_message( @pytest.mark.parametrize( - "hass_config", + ("hass_config", "min_number", "max_number", "step"), [ - { - mqtt.DOMAIN: { - number.DOMAIN: { - "state_topic": "test/state_number", - "command_topic": "test/cmd_number", - "name": "Test Number", - "min": 5, - "max": 110, - "step": 20, + ( + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": "test/state_number", + "command_topic": "test/cmd_number", + "name": "Test Number", + "min": 5, + "max": 110, + "step": 20, + } } - } - } + }, + 5, + 110, + 20, + ), + ( + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": "test/state_number", + "command_topic": "test/cmd_number", + "name": "Test Number", + "min": 100, + "max": 100, + } + } + }, + 100, + 100, + 1, + ), ], ) async def test_min_max_step_attributes( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + min_number: float, + max_number: float, + step: float, ) -> None: """Test min/max/step attributes.""" await mqtt_mock_entry() state = hass.states.get("number.test_number") - assert state.attributes.get(ATTR_MIN) == 5 - assert state.attributes.get(ATTR_MAX) == 110 - assert state.attributes.get(ATTR_STEP) == 20 + assert state.attributes.get(ATTR_MIN) == min_number + assert state.attributes.get(ATTR_MAX) == max_number + assert state.attributes.get(ATTR_STEP) == step @pytest.mark.parametrize( @@ -885,7 +910,7 @@ async def test_invalid_min_max_attributes( ) -> None: """Test invalid min/max attributes.""" assert await mqtt_mock_entry() - assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text + assert f"{CONF_MAX} must be >= {CONF_MIN}" in caplog.text @pytest.mark.parametrize( From d1b78982192d0522a729a6d73ecb6a4e3f7781a2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 8 Apr 2025 15:44:35 +0200 Subject: [PATCH 198/258] Fix small typo in Music Assistant integration causing unavailable players (#142535) Fix small typo in Music Assistant integration causing issues with adding players --- homeassistant/components/music_assistant/media_player.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index 005e5006b52..05fdbe85777 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -151,6 +151,9 @@ async def async_setup_entry( assert event.object_id is not None if event.object_id in added_ids: return + player = mass.players.get(event.object_id) + if TYPE_CHECKING: + assert player is not None if not player.expose_to_ha: return added_ids.add(event.object_id) From e9f02edd8b3901f20190bd5f873bf190d8eec668 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:40:13 +0200 Subject: [PATCH 199/258] Fix adding devices in Husqvarna Automower (#142549) --- homeassistant/components/husqvarna_automower/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 9456074596a..c23ca508916 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -136,6 +136,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]): # Process new device new_devices = current_devices - self._devices_last_update if new_devices: + self.data = data _LOGGER.debug("New devices found: %s", ", ".join(map(str, new_devices))) self._add_new_devices(new_devices) From ec53b61f9e78bb3b6a58b3772907c27248ff579b Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Tue, 8 Apr 2025 16:38:48 -0500 Subject: [PATCH 200/258] Bump pyheos to v1.0.5 (#142554) Update pyheos --- homeassistant/components/heos/manifest.json | 2 +- homeassistant/components/heos/media_player.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index cbac9f20574..8a88913456d 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["pyheos"], "quality_scale": "platinum", - "requirements": ["pyheos==1.0.4"], + "requirements": ["pyheos==1.0.5"], "ssdp": [ { "st": "urn:schemas-denon-com:device:ACT-Denon:1" diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 9cd01051b95..6ba672a57ad 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -87,6 +87,7 @@ BASE_SUPPORTED_FEATURES = ( PLAY_STATE_TO_STATE = { None: MediaPlayerState.IDLE, + PlayState.UNKNOWN: MediaPlayerState.IDLE, PlayState.PLAY: MediaPlayerState.PLAYING, PlayState.STOP: MediaPlayerState.IDLE, PlayState.PAUSE: MediaPlayerState.PAUSED, diff --git a/requirements_all.txt b/requirements_all.txt index d835408a0a6..4be2b5f028a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2005,7 +2005,7 @@ pygti==0.9.4 pyhaversion==22.8.0 # homeassistant.components.heos -pyheos==1.0.4 +pyheos==1.0.5 # homeassistant.components.hive pyhive-integration==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 752e9d08a61..aec2bfe732e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1632,7 +1632,7 @@ pygti==0.9.4 pyhaversion==22.8.0 # homeassistant.components.heos -pyheos==1.0.4 +pyheos==1.0.5 # homeassistant.components.hive pyhive-integration==1.0.2 From a9f85294608bf0b6025585e5b9c3bdf504ba6b82 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Wed, 9 Apr 2025 20:36:41 +0200 Subject: [PATCH 201/258] Fix Quickmode handling in ViCare integration (#142561) * only check quickmode if supported * update snapshot * revert --- homeassistant/components/vicare/fan.py | 27 +++++++++++-------- .../components/vicare/snapshots/test_fan.ambr | 19 +++++++++++-- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/vicare/fan.py b/homeassistant/components/vicare/fan.py index d84b2038dde..88d42503a03 100644 --- a/homeassistant/components/vicare/fan.py +++ b/homeassistant/components/vicare/fan.py @@ -127,6 +127,7 @@ class ViCareFan(ViCareEntity, FanEntity): _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) _attr_translation_key = "ventilation" + _attributes: dict[str, Any] = {} def __init__( self, @@ -155,7 +156,7 @@ class ViCareFan(ViCareEntity, FanEntity): self._attr_supported_features |= FanEntityFeature.SET_SPEED # evaluate quickmodes - quickmodes: list[str] = ( + self._attributes["vicare_quickmodes"] = quickmodes = list[str]( device.getVentilationQuickmodes() if is_supported( "getVentilationQuickmodes", @@ -196,26 +197,23 @@ class ViCareFan(ViCareEntity, FanEntity): @property def is_on(self) -> bool | None: """Return true if the entity is on.""" - if ( - self._attr_supported_features & FanEntityFeature.TURN_OFF - and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY) - ): + if VentilationQuickmode.STANDBY in self._attributes[ + "vicare_quickmodes" + ] and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY): return False return self.percentage is not None and self.percentage > 0 def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - self._api.activateVentilationQuickmode(str(VentilationQuickmode.STANDBY)) @property def icon(self) -> str | None: """Return the icon to use in the frontend.""" - if ( - self._attr_supported_features & FanEntityFeature.TURN_OFF - and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY) - ): + if VentilationQuickmode.STANDBY in self._attributes[ + "vicare_quickmodes" + ] and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY): return "mdi:fan-off" if hasattr(self, "_attr_preset_mode"): if self._attr_preset_mode == VentilationMode.VENTILATION: @@ -242,7 +240,9 @@ class ViCareFan(ViCareEntity, FanEntity): """Set the speed of the fan, as a percentage.""" if self._attr_preset_mode != str(VentilationMode.PERMANENT): self.set_preset_mode(VentilationMode.PERMANENT) - elif self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY): + elif VentilationQuickmode.STANDBY in self._attributes[ + "vicare_quickmodes" + ] and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY): self._api.deactivateVentilationQuickmode(str(VentilationQuickmode.STANDBY)) level = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) @@ -254,3 +254,8 @@ class ViCareFan(ViCareEntity, FanEntity): target_mode = VentilationMode.to_vicare_mode(preset_mode) _LOGGER.debug("changing ventilation mode to %s", target_mode) self._api.activateVentilationMode(target_mode) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Show Device Attributes.""" + return self._attributes diff --git a/tests/components/vicare/snapshots/test_fan.ambr b/tests/components/vicare/snapshots/test_fan.ambr index 2c9e815f7bf..2a44fb87b65 100644 --- a/tests/components/vicare/snapshots/test_fan.ambr +++ b/tests/components/vicare/snapshots/test_fan.ambr @@ -55,6 +55,11 @@ , ]), 'supported_features': , + 'vicare_quickmodes': list([ + 'comfort', + 'eco', + 'holiday', + ]), }), 'context': , 'entity_id': 'fan.model0_ventilation', @@ -94,7 +99,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:fan-off', + 'original_icon': 'mdi:fan', 'original_name': 'Ventilation', 'platform': 'vicare', 'previous_unique_id': None, @@ -108,7 +113,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model1 Ventilation', - 'icon': 'mdi:fan-off', + 'icon': 'mdi:fan', 'percentage': 0, 'percentage_step': 25.0, 'preset_mode': None, @@ -118,6 +123,11 @@ , ]), 'supported_features': , + 'vicare_quickmodes': list([ + 'comfort', + 'eco', + 'holiday', + ]), }), 'context': , 'entity_id': 'fan.model1_ventilation', @@ -179,6 +189,11 @@ , ]), 'supported_features': , + 'vicare_quickmodes': list([ + 'comfort', + 'eco', + 'holiday', + ]), }), 'context': , 'entity_id': 'fan.model2_ventilation', From 60ece3e1c9f6b14a33657ebbb660ec7d510107bb Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:02:51 -0400 Subject: [PATCH 202/258] Fix Core deadlock by ensuring only one ZHA log queue handler thread is running at a time (#142568) Ensure only one log queue handler is running at a time --- homeassistant/components/zha/helpers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 700e2833705..c819f94ceba 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -514,6 +514,7 @@ class ZHAGatewayProxy(EventBase): self._log_queue_handler.listener = logging.handlers.QueueListener( log_simple_queue, log_relay_handler ) + self._log_queue_handler_count: int = 0 self._unsubs: list[Callable[[], None]] = [] self._unsubs.append(self.gateway.on_all_events(self._handle_event_protocol)) @@ -747,7 +748,10 @@ class ZHAGatewayProxy(EventBase): if filterer: self._log_queue_handler.addFilter(filterer) - if self._log_queue_handler.listener: + # Only start a new log queue handler if the old one is no longer running + self._log_queue_handler_count += 1 + + if self._log_queue_handler.listener and self._log_queue_handler_count == 1: self._log_queue_handler.listener.start() for logger_name in DEBUG_RELAY_LOGGERS: @@ -763,7 +767,10 @@ class ZHAGatewayProxy(EventBase): for logger_name in DEBUG_RELAY_LOGGERS: logging.getLogger(logger_name).removeHandler(self._log_queue_handler) - if self._log_queue_handler.listener: + # Only stop the log queue handler if nothing else is using it + self._log_queue_handler_count -= 1 + + if self._log_queue_handler.listener and self._log_queue_handler_count == 0: self._log_queue_handler.listener.stop() if filterer: From d3d78898838c1d69a90cd7312fae3408a99bc285 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 9 Apr 2025 08:51:44 +0200 Subject: [PATCH 203/258] Fix ssl_cert load from config_flow (#142570) fix ssl_cert load from config_flow --- homeassistant/components/daikin/config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index cc25a88ae39..f5febafc4dc 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_UUID from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo +from homeassistant.util.ssl import client_context_no_verify from .const import DOMAIN, KEY_MAC, TIMEOUT @@ -90,6 +91,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): key=key, uuid=uuid, password=password, + ssl_context=client_context_no_verify(), ) except (TimeoutError, ClientError): self.host = None From acdac6d5e82fd687552073123ae6a3b0f4b004d7 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Thu, 10 Apr 2025 20:14:30 +0200 Subject: [PATCH 204/258] Update growatt server dependency to 1.6.0 (#142606) * Update GrowattServer Dependency * Update requirements_test_all.txt --- homeassistant/components/growatt_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 98ceb35ee17..7b3e67228b1 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/growatt_server", "iot_class": "cloud_polling", "loggers": ["growattServer"], - "requirements": ["growattServer==1.5.0"] + "requirements": ["growattServer==1.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4be2b5f028a..28520b20056 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ greenwavereality==0.5.1 gridnet==5.0.1 # homeassistant.components.growatt_server -growattServer==1.5.0 +growattServer==1.6.0 # homeassistant.components.google_sheets gspread==5.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aec2bfe732e..93129fd44a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -929,7 +929,7 @@ greeneye_monitor==3.0.3 gridnet==5.0.1 # homeassistant.components.growatt_server -growattServer==1.5.0 +growattServer==1.6.0 # homeassistant.components.google_sheets gspread==5.5.0 From 82959081de3acb7e1a25b80459edbaa0883f6ad9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Apr 2025 15:48:29 -1000 Subject: [PATCH 205/258] Pin multidict to >= 6.4.2 to resolve memory leaks (#142614) * Pin multidict to >= 6.4.1 to resolve memory leaks https://github.com/aio-libs/multidict/issues/1134 https://github.com/aio-libs/multidict/issues/1131 https://github.com/aio-libs/multidict/releases/tag/v6.4.1 https://github.com/aio-libs/multidict/releases/tag/v6.4.0 * Apply suggestions from code review --- 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 b6dec576591..8eecf0e6f80 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -212,3 +212,8 @@ async-timeout==4.0.3 # 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 +multidict>=6.4.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1be6286d30c..4f8db92b1ba 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -241,6 +241,11 @@ async-timeout==4.0.3 # 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 +multidict>=6.4.2 """ GENERATED_MESSAGE = ( From 2c4461457a29fd9df8db9ae1943a23ff5bae9191 Mon Sep 17 00:00:00 2001 From: henryptung Date: Wed, 9 Apr 2025 19:52:10 -0700 Subject: [PATCH 206/258] Bump led_ble to 1.1.7 (#142629) changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v1.1.6...v1.1.7 --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index b88ef3f029a..14983e5fdfe 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -35,5 +35,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.26.5", "led-ble==1.1.6"] + "requirements": ["bluetooth-data-tools==1.26.5", "led-ble==1.1.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 28520b20056..47eda9ba837 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1314,7 +1314,7 @@ ld2410-ble==0.1.1 leaone-ble==0.1.0 # homeassistant.components.led_ble -led-ble==1.1.6 +led-ble==1.1.7 # homeassistant.components.lektrico lektricowifi==0.0.43 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93129fd44a7..ee4a39dc7d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1111,7 +1111,7 @@ ld2410-ble==0.1.1 leaone-ble==0.1.0 # homeassistant.components.led_ble -led-ble==1.1.6 +led-ble==1.1.7 # homeassistant.components.lektrico lektricowifi==0.0.43 From d1781f57661b5a39b9c5a0065d89627df72c04db Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:05:38 +0200 Subject: [PATCH 207/258] Bump livisi to 0.0.25 (#142638) --- homeassistant/components/livisi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/livisi/manifest.json b/homeassistant/components/livisi/manifest.json index 1077cacf2c4..46ffad162f3 100644 --- a/homeassistant/components/livisi/manifest.json +++ b/homeassistant/components/livisi/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/livisi", "iot_class": "local_polling", - "requirements": ["livisi==0.0.24"] + "requirements": ["livisi==0.0.25"] } diff --git a/requirements_all.txt b/requirements_all.txt index 47eda9ba837..686117734be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1350,7 +1350,7 @@ linear-garage-door==0.2.9 linode-api==4.1.9b1 # homeassistant.components.livisi -livisi==0.0.24 +livisi==0.0.25 # homeassistant.components.google_maps locationsharinglib==5.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee4a39dc7d5..3ac97971908 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1132,7 +1132,7 @@ libsoundtouch==0.8 linear-garage-door==0.2.9 # homeassistant.components.livisi -livisi==0.0.24 +livisi==0.0.25 # homeassistant.components.london_underground london-tube-status==0.5 From d192aecd3b072b8dd59b518604a80e7d83175cb3 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 10 Apr 2025 21:16:53 +0200 Subject: [PATCH 208/258] Comelit config flow timeout error (#142667) --- homeassistant/components/comelit/config_flow.py | 15 ++++++++++++--- homeassistant/components/comelit/strings.json | 6 ++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index f29cc62136b..5854bc1e324 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from asyncio.exceptions import TimeoutError from collections.abc import Mapping from typing import Any @@ -53,10 +54,18 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, try: await api.login() - except aiocomelit_exceptions.CannotConnect as err: - raise CannotConnect from err + except (aiocomelit_exceptions.CannotConnect, TimeoutError) as err: + raise CannotConnect( + translation_domain=DOMAIN, + translation_key="cannot_connect", + translation_placeholders={"error": repr(err)}, + ) from err except aiocomelit_exceptions.CannotAuthenticate as err: - raise InvalidAuth from err + raise InvalidAuth( + translation_domain=DOMAIN, + translation_key="cannot_authenticate", + translation_placeholders={"error": repr(err)}, + ) from err finally: await api.logout() await api.close() diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index f9771e58a40..21f2b89db98 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -69,6 +69,12 @@ }, "invalid_clima_data": { "message": "Invalid 'clima' data" + }, + "cannot_connect": { + "message": "Error connecting: {error}" + }, + "cannot_authenticate": { + "message": "Error authenticating: {error}" } } } From b40bdab0aefe0785365343e9e94ad23ce1cbc5e8 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 10 Apr 2025 23:11:35 +0200 Subject: [PATCH 209/258] Fix EC certificate key not allowed in MQTT client setup (#142698) --- homeassistant/components/mqtt/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 7fe01e9a890..5947bfb3da9 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1611,6 +1611,7 @@ def async_is_pem_data(data: bytes) -> bool: return ( b"-----BEGIN CERTIFICATE-----" in data or b"-----BEGIN PRIVATE KEY-----" in data + or b"-----BEGIN EC PRIVATE KEY-----" in data or b"-----BEGIN RSA PRIVATE KEY-----" in data or b"-----BEGIN ENCRYPTED PRIVATE KEY-----" in data ) From 4c1d32020a1d3f400acbf158d2284892789f44c8 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:32:54 +0200 Subject: [PATCH 210/258] Bump PyViCare to 2.44.0 (#142701) bump vicare to v2.44.0 --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index e39adaf6c4c..fed777e6435 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "iot_class": "cloud_polling", "loggers": ["PyViCare"], - "requirements": ["PyViCare==2.43.1"] + "requirements": ["PyViCare==2.44.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 686117734be..9413259c7c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.7.5 # homeassistant.components.vicare -PyViCare==2.43.1 +PyViCare==2.44.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ac97971908..0b9629299d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.7.5 # homeassistant.components.vicare -PyViCare==2.43.1 +PyViCare==2.44.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 From fd2987a9fd883c33fa54c44fd66fc1875e1be1fa Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 11 Apr 2025 10:22:30 +0200 Subject: [PATCH 211/258] Bump reolink-aio 0.13.1 (#142719) --- 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 82b9586cccc..9105dfda66f 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.0"] + "requirements": ["reolink-aio==0.13.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9413259c7c4..86fc127a23c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2627,7 +2627,7 @@ renault-api==0.2.9 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.0 +reolink-aio==0.13.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b9629299d9..e27c99280ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2128,7 +2128,7 @@ renault-api==0.2.9 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.0 +reolink-aio==0.13.1 # homeassistant.components.rflink rflink==0.0.66 From 7696b101f6856e908e80ad96a45e5d30bb8b982b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 11 Apr 2025 11:42:18 +0200 Subject: [PATCH 212/258] Reolink migrate unique ID debugging (#142723) * Filter out unexpected unique_ids * correct * Add test * fix styling --- homeassistant/components/reolink/__init__.py | 8 ++++++++ tests/components/reolink/test_init.py | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 99ca91c5bdf..c326f1120c9 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -420,6 +420,14 @@ def migrate_entity_ids( if entity.device_id in ch_device_ids: ch = ch_device_ids[entity.device_id] id_parts = entity.unique_id.split("_", 2) + if len(id_parts) < 3: + _LOGGER.warning( + "Reolink channel %s entity has unexpected unique_id format %s, with device id %s", + ch, + entity.unique_id, + entity.device_id, + ) + continue if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch): new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}" existing_entity = entity_reg.async_get_entity_id( diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 4c4908dca6f..5915bd06608 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -424,6 +424,15 @@ async def test_removing_chime( True, True, ), + ( + f"{TEST_UID}_unexpected", + f"{TEST_UID}_unexpected", + f"{TEST_UID}_{TEST_UID_CAM}", + f"{TEST_UID}_{TEST_UID_CAM}", + Platform.SWITCH, + True, + True, + ), ], ) async def test_migrate_entity_ids( @@ -469,7 +478,8 @@ async def test_migrate_entity_ids( ) assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) - assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) is None + if original_id != new_id: + assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) is None assert device_registry.async_get_device(identifiers={(DOMAIN, original_dev_id)}) if new_dev_id != original_dev_id: @@ -482,7 +492,8 @@ async def test_migrate_entity_ids( 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, original_id) is None + if original_id != new_id: + assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) is None assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) if new_dev_id != original_dev_id: From 494849988976464a8ef864d4a8ff215375b02865 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 11 Apr 2025 17:25:26 +0200 Subject: [PATCH 213/258] Update frontend to 20250411.0 (#142736) --- 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 140d90c5dbe..64b49588ba1 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==20250404.0"] + "requirements": ["home-assistant-frontend==20250411.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8eecf0e6f80..95502bf257c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.37.0 hass-nabucasa==0.94.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250404.0 +home-assistant-frontend==20250411.0 home-assistant-intents==2025.3.28 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 86fc127a23c..207a95a6a95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250404.0 +home-assistant-frontend==20250411.0 # homeassistant.components.conversation home-assistant-intents==2025.3.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e27c99280ad..81358b073b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ hole==0.8.0 holidays==0.69 # homeassistant.components.frontend -home-assistant-frontend==20250404.0 +home-assistant-frontend==20250411.0 # homeassistant.components.conversation home-assistant-intents==2025.3.28 From b5d541b5963dfae805abc78a40f94087b1c22531 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Apr 2025 16:51:32 +0200 Subject: [PATCH 214/258] Bump pySmartThings to 3.0.4 (#142739) --- 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 dda7ef53cf5..4cd27e49664 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.0.2"] + "requirements": ["pysmartthings==3.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 207a95a6a95..962c426cfc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2319,7 +2319,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.0.2 +pysmartthings==3.0.4 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81358b073b7..99a77f19a46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1889,7 +1889,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.0.2 +pysmartthings==3.0.4 # homeassistant.components.smarty pysmarty2==0.10.2 From 28d776a0b02404418c0a0832aa80d96f57852385 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Apr 2025 17:24:39 +0200 Subject: [PATCH 215/258] Fix SmartThings gas meter (#142741) --- .../components/smartthings/sensor.py | 6 +- tests/components/smartthings/conftest.py | 1 + .../fixtures/device_status/gas_meter.json | 61 ++++++ .../fixtures/devices/gas_meter.json | 56 +++++ .../smartthings/snapshots/test_init.ambr | 33 +++ .../smartthings/snapshots/test_sensor.ambr | 202 ++++++++++++++++++ 6 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 tests/components/smartthings/fixtures/device_status/gas_meter.json create mode 100644 tests/components/smartthings/fixtures/devices/gas_meter.json diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 346516be480..e081f35d0e0 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -413,7 +413,6 @@ CAPABILITY_TO_SENSORS: dict[ ) ] }, - # Haven't seen at devices yet Capability.GAS_METER: { Attribute.GAS_METER: [ SmartThingsSensorEntityDescription( @@ -421,7 +420,7 @@ CAPABILITY_TO_SENSORS: dict[ translation_key="gas_meter", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ) ], Attribute.GAS_METER_CALORIFIC: [ @@ -443,7 +442,7 @@ CAPABILITY_TO_SENSORS: dict[ key=Attribute.GAS_METER_VOLUME, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ) ], }, @@ -1003,6 +1002,7 @@ CAPABILITY_TO_SENSORS: dict[ UNITS = { "C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT, + "ccf": UnitOfVolume.CENTUM_CUBIC_FEET, "lux": LIGHT_LUX, "mG": None, "μg/m^3": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 277c327744f..26af812fe1f 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -146,6 +146,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "ikea_kadrilj", "aux_ac", "hw_q80r_soundbar", + "gas_meter", ] ) def device_fixture( diff --git a/tests/components/smartthings/fixtures/device_status/gas_meter.json b/tests/components/smartthings/fixtures/device_status/gas_meter.json new file mode 100644 index 00000000000..dc7f9b2e0c3 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/gas_meter.json @@ -0,0 +1,61 @@ +{ + "components": { + "main": { + "healthCheck": { + "checkInterval": { + "value": 60, + "unit": "s", + "data": { + "deviceScheme": "UNTRACKED", + "protocol": "cloud" + }, + "timestamp": "2025-02-27T14:06:11.704Z" + }, + "healthStatus": { + "value": null + }, + "DeviceWatch-Enroll": { + "value": null + }, + "DeviceWatch-DeviceStatus": { + "value": "online", + "data": {}, + "timestamp": "2025-04-11T13:00:00.444Z" + } + }, + "refresh": {}, + "gasMeter": { + "gasMeterPrecision": { + "value": { + "volume": 5, + "calorific": 1, + "conversion": 1 + }, + "timestamp": "2025-04-11T13:00:00.444Z" + }, + "gasMeterCalorific": { + "value": 40, + "timestamp": "2025-04-11T13:00:00.444Z" + }, + "gasMeterTime": { + "value": "2025-04-11T13:30:00.028Z", + "timestamp": "2025-04-11T13:30:00.532Z" + }, + "gasMeterVolume": { + "value": 14, + "unit": "ccf", + "timestamp": "2025-04-11T13:00:00.444Z" + }, + "gasMeterConversion": { + "value": 3.6, + "timestamp": "2025-04-11T13:00:00.444Z" + }, + "gasMeter": { + "value": 450.5, + "unit": "kWh", + "timestamp": "2025-04-11T13:00:00.444Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/gas_meter.json b/tests/components/smartthings/fixtures/devices/gas_meter.json new file mode 100644 index 00000000000..9bf8af654c7 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/gas_meter.json @@ -0,0 +1,56 @@ +{ + "items": [ + { + "deviceId": "3b57dca3-9a90-4f27-ba80-f947b1e60d58", + "name": "copper_gas_meter_v04", + "label": "Gas Meter", + "manufacturerName": "0A6v", + "presentationId": "ST_176e9efa-01d2-4d1b-8130-d37a4ef1b413", + "deviceManufacturerCode": "CopperLabs", + "locationId": "4e88bf74-3bed-4e6d-9fa7-6acb776a4df9", + "ownerId": "6fc21de5-123e-2f8c-2cc6-311635aeaaef", + "roomId": "fafae9db-a2b5-480f-8ff5-df8f913356df", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "healthCheck", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "gasMeter", + "version": 1 + } + ], + "categories": [ + { + "name": "GasMeter", + "categoryType": "manufacturer" + } + ] + } + ], + "createTime": "2025-02-27T14:06:11.522Z", + "profile": { + "id": "5cca2553-23d6-43c4-81ad-a1c6c43efa00" + }, + "viper": { + "manufacturerName": "CopperLabs", + "modelName": "Virtual Gas Meter", + "endpointAppId": "viper_1d5767a0-af08-11ed-a999-9f1f172a27ff" + }, + "type": "VIPER", + "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 8ec97af7d84..db8c3a6ccc5 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -1058,6 +1058,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[gas_meter] + 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', + '3b57dca3-9a90-4f27-ba80-f947b1e60d58', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'CopperLabs', + 'model': 'Virtual Gas Meter', + 'model_id': None, + 'name': 'Gas Meter', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[ge_in_wall_smart_dimmer] DeviceRegistryEntrySnapshot({ 'area_id': 'theater', diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 8ace345be18..e9441f2e408 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -8007,6 +8007,208 @@ 'state': 'unknown', }) # --- +# name: test_all_entities[gas_meter][sensor.gas_meter_gas-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.gas_meter_gas', + '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': 'Gas', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '3b57dca3-9a90-4f27-ba80-f947b1e60d58_main_gasMeter_gasMeterVolume_gasMeterVolume', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[gas_meter][sensor.gas_meter_gas-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gas Meter Gas', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gas_meter_gas', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_all_entities[gas_meter][sensor.gas_meter_gas_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.gas_meter_gas_meter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas meter', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gas_meter', + 'unique_id': '3b57dca3-9a90-4f27-ba80-f947b1e60d58_main_gasMeter_gasMeter_gasMeter', + 'unit_of_measurement': 'kWh', + }) +# --- +# name: test_all_entities[gas_meter][sensor.gas_meter_gas_meter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Gas Meter Gas meter', + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.gas_meter_gas_meter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '450.5', + }) +# --- +# name: test_all_entities[gas_meter][sensor.gas_meter_gas_meter_calorific-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.gas_meter_gas_meter_calorific', + 'has_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 meter calorific', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gas_meter_calorific', + 'unique_id': '3b57dca3-9a90-4f27-ba80-f947b1e60d58_main_gasMeter_gasMeterCalorific_gasMeterCalorific', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[gas_meter][sensor.gas_meter_gas_meter_calorific-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Gas Meter Gas meter calorific', + }), + 'context': , + 'entity_id': 'sensor.gas_meter_gas_meter_calorific', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_all_entities[gas_meter][sensor.gas_meter_gas_meter_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.gas_meter_gas_meter_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': 'Gas meter time', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gas_meter_time', + 'unique_id': '3b57dca3-9a90-4f27-ba80-f947b1e60d58_main_gasMeter_gasMeterTime_gasMeterTime', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[gas_meter][sensor.gas_meter_gas_meter_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Gas Meter Gas meter time', + }), + 'context': , + 'entity_id': 'sensor.gas_meter_gas_meter_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-11T13:30:00+00:00', + }) +# --- # name: test_all_entities[generic_ef00_v1][sensor.thermostat_kuche_link_quality-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From d868f39aea0df06bbf3bd1e94e218c8abe5850f4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 11 Apr 2025 08:30:12 -0700 Subject: [PATCH 216/258] Fix Anthropic bug parsing a streaming response with no json (#142745) --- .../components/anthropic/conversation.py | 2 +- .../components/anthropic/test_conversation.py | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anthropic/conversation.py b/homeassistant/components/anthropic/conversation.py index 5e5ad464eaa..288ec63509e 100644 --- a/homeassistant/components/anthropic/conversation.py +++ b/homeassistant/components/anthropic/conversation.py @@ -266,7 +266,7 @@ async def _transform_stream( raise ValueError("Unexpected stop event without a current block") if current_block["type"] == "tool_use": tool_block = cast(ToolUseBlockParam, current_block) - tool_args = json.loads(current_tool_args) + tool_args = json.loads(current_tool_args) if current_tool_args else {} tool_block["input"] = tool_args yield { "tool_calls": [ diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py index 67a4434a664..caaef43e931 100644 --- a/tests/components/anthropic/test_conversation.py +++ b/tests/components/anthropic/test_conversation.py @@ -303,11 +303,27 @@ async def test_conversation_agent( @patch("homeassistant.components.anthropic.conversation.llm.AssistAPI._async_get_tools") +@pytest.mark.parametrize( + ("tool_call_json_parts", "expected_call_tool_args"), + [ + ( + ['{"param1": "test_value"}'], + {"param1": "test_value"}, + ), + ( + ['{"para', 'm1": "test_valu', 'e"}'], + {"param1": "test_value"}, + ), + ([""], {}), + ], +) async def test_function_call( mock_get_tools, hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + tool_call_json_parts: list[str], + expected_call_tool_args: dict[str, Any], ) -> None: """Test function call from the assistant.""" agent_id = "conversation.claude" @@ -343,7 +359,7 @@ async def test_function_call( 1, "toolu_0123456789AbCdEfGhIjKlM", "test_tool", - ['{"para', 'm1": "test_valu', 'e"}'], + tool_call_json_parts, ), ] ) @@ -387,7 +403,7 @@ async def test_function_call( llm.ToolInput( id="toolu_0123456789AbCdEfGhIjKlM", tool_name="test_tool", - tool_args={"param1": "test_value"}, + tool_args=expected_call_tool_args, ), llm.LLMContext( platform="anthropic", From 933d008e52635cf3309986a0371735f2ba9d3926 Mon Sep 17 00:00:00 2001 From: Jeff Rescignano Date: Fri, 11 Apr 2025 12:07:27 -0400 Subject: [PATCH 217/258] Upgrade sharkiq depedency to 1.1.0 (#142746) --- homeassistant/components/sharkiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sharkiq/manifest.json b/homeassistant/components/sharkiq/manifest.json index 0e07dd96902..9f9009693e5 100644 --- a/homeassistant/components/sharkiq/manifest.json +++ b/homeassistant/components/sharkiq/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sharkiq", "iot_class": "cloud_polling", "loggers": ["sharkiq"], - "requirements": ["sharkiq==1.0.2"] + "requirements": ["sharkiq==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 962c426cfc0..d689114d187 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2730,7 +2730,7 @@ sentry-sdk==1.45.1 sfrbox-api==0.0.11 # homeassistant.components.sharkiq -sharkiq==1.0.2 +sharkiq==1.1.0 # homeassistant.components.aquostv sharp_aquos_rc==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99a77f19a46..c6df7040f83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2207,7 +2207,7 @@ sentry-sdk==1.45.1 sfrbox-api==0.0.11 # homeassistant.components.sharkiq -sharkiq==1.0.2 +sharkiq==1.1.0 # homeassistant.components.simplefin simplefin4py==0.0.18 From 667cb772e9e862a1009586ddbd2257255a209162 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Apr 2025 16:12:45 +0000 Subject: [PATCH 218/258] Bump version to 2025.4.2 --- 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 12925dea143..ec6a6313542 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 = 4 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __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, 0) diff --git a/pyproject.toml b/pyproject.toml index 0c5a974a2aa..901ad944bec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.1" +version = "2025.4.2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From c7c645776d518af6cf60531b8a09ecce965379c3 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 4 Apr 2025 07:21:40 +0200 Subject: [PATCH 219/258] Bump ical to 9.1.0 (#142197) --- 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 efce97a0d6f..2bedc7a3163 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.0.3"] + "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 528552aaa57..90cd5a6d2ac 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.0.3"] + "requirements": ["ical==9.1.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 6f117131c20..a630c18c669 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.0.3"] + "requirements": ["ical==9.1.0"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 256f5baf0ff..da078395484 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.0.3"] + "requirements": ["ical==9.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d689114d187..ef0d477eda3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1196,7 +1196,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.0.3 +ical==9.1.0 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6df7040f83..ac142674d20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1014,7 +1014,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.0.3 +ical==9.1.0 # homeassistant.components.caldav icalendar==6.1.0 From 7a1bea7ff5c6d79bb3f054f954f7ef08e5242123 Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Tue, 8 Apr 2025 17:50:36 +1200 Subject: [PATCH 220/258] Add jaraco.itertools license exception as the classifier was removed but no SPDX expression was added (#142439) --- script/licenses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/licenses.py b/script/licenses.py index 448e9dd2a67..62e1845b911 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -190,6 +190,7 @@ EXCEPTIONS = { "enocean", # https://github.com/kipe/enocean/pull/142 "imutils", # https://github.com/PyImageSearch/imutils/pull/292 "iso4217", # Public domain + "jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21 "kiwiki_client", # https://github.com/c7h/kiwiki_client/pull/6 "ld2410-ble", # https://github.com/930913/ld2410-ble/pull/7 "maxcube-api", # https://github.com/uebelack/python-maxcube-api/pull/48 From 51f5ce013f153b8ce72b7b35e833e4c77f82a28c Mon Sep 17 00:00:00 2001 From: Chase Mamatey Date: Sat, 12 Apr 2025 19:00:49 -0400 Subject: [PATCH 221/258] Fix duke_energy data retrieval to adhere to service start date (#136054) --- .../components/duke_energy/coordinator.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/duke_energy/coordinator.py b/homeassistant/components/duke_energy/coordinator.py index a76168475c0..a70c94e6fee 100644 --- a/homeassistant/components/duke_energy/coordinator.py +++ b/homeassistant/components/duke_energy/coordinator.py @@ -179,22 +179,18 @@ class DukeEnergyCoordinator(DataUpdateCoordinator[None]): one = timedelta(days=1) if start_time is None: # Max 3 years of data - agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"]) - if agreement_date is None: - start = dt_util.now(tz) - timedelta(days=3 * 365) - else: - start = max( - agreement_date.replace(tzinfo=tz), - dt_util.now(tz) - timedelta(days=3 * 365), - ) + start = dt_util.now(tz) - timedelta(days=3 * 365) else: start = datetime.fromtimestamp(start_time, tz=tz) - lookback + agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"]) + if agreement_date is not None: + start = max(agreement_date.replace(tzinfo=tz), start) start = start.replace(hour=0, minute=0, second=0, microsecond=0) end = dt_util.now(tz).replace(hour=0, minute=0, second=0, microsecond=0) - one _LOGGER.debug("Data lookup range: %s - %s", start, end) - start_step = end - lookback + start_step = max(end - lookback, start) end_step = end usage: dict[datetime, dict[str, float | int]] = {} while True: From eda642554d43bb0255422316ce8aad12b867a8c8 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 13 Apr 2025 21:55:16 +1000 Subject: [PATCH 222/258] Check Energy Live API works before creating the coordinator in Tessie (#142510) * Check live API works before creating the coordinator * Fix diag * Fix mypy on entity * is not None --- homeassistant/components/tessie/__init__.py | 24 ++++++++++++++++--- .../components/tessie/binary_sensor.py | 2 ++ .../components/tessie/coordinator.py | 12 +++++++++- .../components/tessie/diagnostics.py | 4 +++- homeassistant/components/tessie/entity.py | 2 +- homeassistant/components/tessie/models.py | 2 +- homeassistant/components/tessie/sensor.py | 9 +++++-- 7 files changed, 46 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index e247931e3ba..7fd2729ef03 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -6,7 +6,12 @@ import logging from aiohttp import ClientError, ClientResponseError from tesla_fleet_api.const import Scope -from tesla_fleet_api.exceptions import TeslaFleetError +from tesla_fleet_api.exceptions import ( + Forbidden, + InvalidToken, + SubscriptionRequired, + TeslaFleetError, +) from tesla_fleet_api.tessie import Tessie from tessie_api import get_state_of_all_vehicles @@ -124,12 +129,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo continue api = tessie.energySites.create(site_id) + + try: + live_status = (await api.live_status())["response"] + except (InvalidToken, Forbidden, SubscriptionRequired) as e: + raise ConfigEntryAuthFailed from e + except TeslaFleetError as e: + raise ConfigEntryNotReady(e.message) from e + energysites.append( TessieEnergyData( api=api, id=site_id, - live_coordinator=TessieEnergySiteLiveCoordinator( - hass, entry, api + live_coordinator=( + TessieEnergySiteLiveCoordinator( + hass, entry, api, live_status + ) + if isinstance(live_status, dict) + else None ), info_coordinator=TessieEnergySiteInfoCoordinator( hass, entry, api @@ -147,6 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo *( energysite.live_coordinator.async_config_entry_first_refresh() for energysite in energysites + if energysite.live_coordinator is not None ), *( energysite.info_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 515339c3da8..cdf3b0035fc 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -191,6 +191,7 @@ async def async_setup_entry( TessieEnergyLiveBinarySensorEntity(energy, description) for energy in entry.runtime_data.energysites for description in ENERGY_LIVE_DESCRIPTIONS + if energy.live_coordinator is not None ), ( TessieEnergyInfoBinarySensorEntity(vehicle, description) @@ -233,6 +234,7 @@ class TessieEnergyLiveBinarySensorEntity(TessieEnergyEntity, BinarySensorEntity) ) -> None: """Initialize the binary sensor.""" self.entity_description = description + assert data.live_coordinator is not None super().__init__(data, data.live_coordinator, description.key) def _async_update_attrs(self) -> None: diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index 2382595b058..8b6fb639a64 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -102,7 +102,11 @@ class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]): config_entry: TessieConfigEntry def __init__( - self, hass: HomeAssistant, config_entry: TessieConfigEntry, api: EnergySite + self, + hass: HomeAssistant, + config_entry: TessieConfigEntry, + api: EnergySite, + data: dict[str, Any], ) -> None: """Initialize Tessie Energy Site Live coordinator.""" super().__init__( @@ -114,6 +118,12 @@ class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) self.api = api + # Convert Wall Connectors from array to dict + data["wall_connectors"] = { + wc["din"]: wc for wc in (data.get("wall_connectors") or []) + } + self.data = data + async def _async_update_data(self) -> dict[str, Any]: """Update energy site data using Tessie API.""" diff --git a/homeassistant/components/tessie/diagnostics.py b/homeassistant/components/tessie/diagnostics.py index bd2db772b57..21fc208612d 100644 --- a/homeassistant/components/tessie/diagnostics.py +++ b/homeassistant/components/tessie/diagnostics.py @@ -41,7 +41,9 @@ async def async_get_config_entry_diagnostics( ] energysites = [ { - "live": async_redact_data(x.live_coordinator.data, ENERGY_LIVE_REDACT), + "live": async_redact_data(x.live_coordinator.data, ENERGY_LIVE_REDACT) + if x.live_coordinator + else None, "info": async_redact_data(x.info_coordinator.data, ENERGY_INFO_REDACT), } for x in entry.runtime_data.energysites diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index a2b6d3c9761..fb49d02f42e 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -155,7 +155,7 @@ class TessieWallConnectorEntity(TessieBaseEntity): via_device=(DOMAIN, str(data.id)), serial_number=din.split("-")[-1], ) - + assert data.live_coordinator super().__init__(data.live_coordinator, key) @property diff --git a/homeassistant/components/tessie/models.py b/homeassistant/components/tessie/models.py index 03652782cfe..5330d2d0bf0 100644 --- a/homeassistant/components/tessie/models.py +++ b/homeassistant/components/tessie/models.py @@ -28,7 +28,7 @@ class TessieEnergyData: """Data for a Energy Site in the Tessie integration.""" api: EnergySite - live_coordinator: TessieEnergySiteLiveCoordinator + live_coordinator: TessieEnergySiteLiveCoordinator | None info_coordinator: TessieEnergySiteInfoCoordinator id: int device: DeviceInfo diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index e5b476057fa..52accb15575 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -396,12 +396,16 @@ async def async_setup_entry( TessieEnergyLiveSensorEntity(energysite, description) for energysite in entry.runtime_data.energysites for description in ENERGY_LIVE_DESCRIPTIONS - if description.key in energysite.live_coordinator.data - or description.key == "percentage_charged" + if energysite.live_coordinator is not None + and ( + description.key in energysite.live_coordinator.data + or description.key == "percentage_charged" + ) ), ( # Add wall connectors TessieWallConnectorSensorEntity(energysite, din, description) for energysite in entry.runtime_data.energysites + if energysite.live_coordinator is not None for din in energysite.live_coordinator.data.get("wall_connectors", {}) for description in WALL_CONNECTOR_DESCRIPTIONS ), @@ -446,6 +450,7 @@ class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity): ) -> None: """Initialize the sensor.""" self.entity_description = description + assert data.live_coordinator is not None super().__init__(data, data.live_coordinator, description.key) def _async_update_attrs(self) -> None: From c1c5776d858d01f308f926c2e76bb246232b4201 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Apr 2025 16:46:30 +0200 Subject: [PATCH 223/258] Correct enum member check in home_connect (#142666) * Correct enum member check in home_connect * Update homeassistant/components/home_connect/coordinator.py Co-authored-by: Martin Hjelmare * Add mypy override --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/home_connect/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/coordinator.py b/homeassistant/components/home_connect/coordinator.py index edaf5ea7332..05a6a592ae8 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -204,7 +204,7 @@ class HomeConnectCoordinator( events = self.data[event_message_ha_id].events for event in event_message.data.items: event_key = event.key - if event_key in SettingKey: + if event_key in SettingKey.__members__.values(): # type: ignore[comparison-overlap] setting_key = SettingKey(event_key) if setting_key in settings: settings[setting_key].value = event.value From 33a185dade4fee7d1cc395dfeddc9f24f028c1af Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 11 Apr 2025 19:39:40 +0200 Subject: [PATCH 224/258] Fix error in recurrence calculation of Habitica integration (#142759) Fix error in rrule calculation of Habitica integration --- homeassistant/components/habitica/util.py | 2 +- tests/components/habitica/fixtures/tasks.json | 43 +++ tests/components/habitica/fixtures/user.json | 3 +- .../habitica/snapshots/test_calendar.ambr | 28 ++ .../habitica/snapshots/test_services.ambr | 300 ++++++++++++++++++ .../habitica/snapshots/test_todo.ambr | 9 +- 6 files changed, 382 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py index 757c675b045..1ca908eb3ff 100644 --- a/homeassistant/components/habitica/util.py +++ b/homeassistant/components/habitica/util.py @@ -74,7 +74,7 @@ def build_rrule(task: TaskData) -> rrule: bysetpos = None if rrule_frequency == MONTHLY and task.weeksOfMonth: - bysetpos = task.weeksOfMonth + bysetpos = [i + 1 for i in task.weeksOfMonth] weekdays = weekdays if weekdays else [MO] return rrule( diff --git a/tests/components/habitica/fixtures/tasks.json b/tests/components/habitica/fixtures/tasks.json index 085508b4432..ecbe0a1f86d 100644 --- a/tests/components/habitica/fixtures/tasks.json +++ b/tests/components/habitica/fixtures/tasks.json @@ -624,6 +624,49 @@ "isDue": false, "id": "6e53f1f5-a315-4edd-984d-8d762e4a08ef" }, + { + "repeat": { + "m": false, + "t": false, + "w": false, + "th": false, + "f": false, + "s": false, + "su": true + }, + "challenge": {}, + "group": { + "completedBy": {}, + "assignedUsers": [] + }, + "_id": "369afeed-61e3-4bf7-9747-66e05807134c", + "frequency": "monthly", + "everyX": 1, + "streak": 1, + "nextDue": ["2024-12-14T23:00:00.000Z", "2025-01-18T23:00:00.000Z"], + "yesterDaily": true, + "history": [], + "completed": false, + "collapseChecklist": false, + "type": "daily", + "text": "Monatliche Finanzübersicht erstellen", + "notes": "Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.", + "tags": [], + "value": -0.9215181434950852, + "priority": 1, + "attribute": "str", + "byHabitica": false, + "startDate": "2024-04-04T22:00:00.000Z", + "daysOfMonth": [], + "weeksOfMonth": [0], + "checklist": [], + "reminders": [], + "createdAt": "2024-04-04T22:00:00.000Z", + "updatedAt": "2024-04-04T22:00:00.000Z", + "userId": "5f359083-ef78-4af0-985a-0b2c6d05797c", + "isDue": false, + "id": "369afeed-61e3-4bf7-9747-66e05807134c" + }, { "repeat": { "m": false, diff --git a/tests/components/habitica/fixtures/user.json b/tests/components/habitica/fixtures/user.json index 58eca2837b6..d2f0091b6dd 100644 --- a/tests/components/habitica/fixtures/user.json +++ b/tests/components/habitica/fixtures/user.json @@ -66,7 +66,8 @@ "564b9ac9-c53d-4638-9e7f-1cd96fe19baa", "f2c85972-1a19-4426-bc6d-ce3337b9d99f", "2c6d136c-a1c3-4bef-b7c4-fa980784b1e1", - "6e53f1f5-a315-4edd-984d-8d762e4a08ef" + "6e53f1f5-a315-4edd-984d-8d762e4a08ef", + "369afeed-61e3-4bf7-9747-66e05807134c" ], "habits": ["1d147de6-5c02-4740-8e2f-71d3015a37f4"] }, diff --git a/tests/components/habitica/snapshots/test_calendar.ambr b/tests/components/habitica/snapshots/test_calendar.ambr index 2948f31f1cf..c7f12684efe 100644 --- a/tests/components/habitica/snapshots/test_calendar.ambr +++ b/tests/components/habitica/snapshots/test_calendar.ambr @@ -87,6 +87,20 @@ 'summary': 'Fitnessstudio besuchen', 'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', }), + dict({ + 'description': 'Klicke um den Namen Deines aktuellen Projekts anzugeben & setze einen Terminplan!', + 'end': dict({ + 'date': '2024-09-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': 'FREQ=MONTHLY;BYSETPOS=4;BYDAY=SU', + 'start': dict({ + 'date': '2024-09-22', + }), + 'summary': 'Arbeite an einem kreativen Projekt', + 'uid': '6e53f1f5-a315-4edd-984d-8d762e4a08ef', + }), dict({ 'description': 'Klicke um Änderungen zu machen!', 'end': dict({ @@ -563,6 +577,20 @@ 'summary': 'Fitnessstudio besuchen', 'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', }), + dict({ + 'description': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.', + 'end': dict({ + 'date': '2024-10-07', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': 'FREQ=MONTHLY;BYSETPOS=1;BYDAY=SU', + 'start': dict({ + 'date': '2024-10-06', + }), + 'summary': 'Monatliche Finanzübersicht erstellen', + 'uid': '369afeed-61e3-4bf7-9747-66e05807134c', + }), dict({ 'description': 'Klicke um Änderungen zu machen!', 'end': dict({ diff --git a/tests/components/habitica/snapshots/test_services.ambr b/tests/components/habitica/snapshots/test_services.ambr index 430cd379c0d..9fbb6a43e94 100644 --- a/tests/components/habitica/snapshots/test_services.ambr +++ b/tests/components/habitica/snapshots/test_services.ambr @@ -1193,6 +1193,81 @@ ]), 'yesterDaily': True, }), + dict({ + 'alias': None, + 'attribute': 'str', + 'byHabitica': False, + 'challenge': dict({ + 'broken': None, + 'id': None, + 'shortName': None, + 'taskId': None, + 'winner': None, + }), + 'checklist': list([ + ]), + 'collapseChecklist': False, + 'completed': False, + 'counterDown': None, + 'counterUp': None, + 'createdAt': '2024-04-04T22:00:00+00:00', + 'date': None, + 'daysOfMonth': list([ + ]), + 'down': None, + 'everyX': 1, + 'frequency': 'monthly', + 'group': dict({ + 'assignedDate': None, + 'assignedUsers': list([ + ]), + 'assignedUsersDetail': dict({ + }), + 'assigningUsername': None, + 'completedBy': dict({ + 'date': None, + 'userId': None, + }), + 'id': None, + 'managerNotes': None, + 'taskId': None, + }), + 'history': list([ + ]), + 'id': '369afeed-61e3-4bf7-9747-66e05807134c', + 'isDue': False, + 'nextDue': list([ + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + ]), + 'notes': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.', + 'priority': 1, + 'reminders': list([ + ]), + 'repeat': dict({ + 'f': False, + 'm': False, + 's': False, + 'su': True, + 't': False, + 'th': False, + 'w': False, + }), + 'startDate': '2024-04-04T22:00:00+00:00', + 'streak': 1, + 'tags': list([ + ]), + 'text': 'Monatliche Finanzübersicht erstellen', + 'type': 'daily', + 'up': None, + 'updatedAt': '2024-04-04T22:00:00+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', + 'value': -0.9215181434950852, + 'weeksOfMonth': list([ + 0, + ]), + 'yesterDaily': True, + }), dict({ 'alias': None, 'attribute': 'str', @@ -3465,6 +3540,81 @@ ]), 'yesterDaily': True, }), + dict({ + 'alias': None, + 'attribute': 'str', + 'byHabitica': False, + 'challenge': dict({ + 'broken': None, + 'id': None, + 'shortName': None, + 'taskId': None, + 'winner': None, + }), + 'checklist': list([ + ]), + 'collapseChecklist': False, + 'completed': False, + 'counterDown': None, + 'counterUp': None, + 'createdAt': '2024-04-04T22:00:00+00:00', + 'date': None, + 'daysOfMonth': list([ + ]), + 'down': None, + 'everyX': 1, + 'frequency': 'monthly', + 'group': dict({ + 'assignedDate': None, + 'assignedUsers': list([ + ]), + 'assignedUsersDetail': dict({ + }), + 'assigningUsername': None, + 'completedBy': dict({ + 'date': None, + 'userId': None, + }), + 'id': None, + 'managerNotes': None, + 'taskId': None, + }), + 'history': list([ + ]), + 'id': '369afeed-61e3-4bf7-9747-66e05807134c', + 'isDue': False, + 'nextDue': list([ + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + ]), + 'notes': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.', + 'priority': 1, + 'reminders': list([ + ]), + 'repeat': dict({ + 'f': False, + 'm': False, + 's': False, + 'su': True, + 't': False, + 'th': False, + 'w': False, + }), + 'startDate': '2024-04-04T22:00:00+00:00', + 'streak': 1, + 'tags': list([ + ]), + 'text': 'Monatliche Finanzübersicht erstellen', + 'type': 'daily', + 'up': None, + 'updatedAt': '2024-04-04T22:00:00+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', + 'value': -0.9215181434950852, + 'weeksOfMonth': list([ + 0, + ]), + 'yesterDaily': True, + }), dict({ 'alias': None, 'attribute': 'str', @@ -4608,6 +4758,81 @@ ]), 'yesterDaily': True, }), + dict({ + 'alias': None, + 'attribute': 'str', + 'byHabitica': False, + 'challenge': dict({ + 'broken': None, + 'id': None, + 'shortName': None, + 'taskId': None, + 'winner': None, + }), + 'checklist': list([ + ]), + 'collapseChecklist': False, + 'completed': False, + 'counterDown': None, + 'counterUp': None, + 'createdAt': '2024-04-04T22:00:00+00:00', + 'date': None, + 'daysOfMonth': list([ + ]), + 'down': None, + 'everyX': 1, + 'frequency': 'monthly', + 'group': dict({ + 'assignedDate': None, + 'assignedUsers': list([ + ]), + 'assignedUsersDetail': dict({ + }), + 'assigningUsername': None, + 'completedBy': dict({ + 'date': None, + 'userId': None, + }), + 'id': None, + 'managerNotes': None, + 'taskId': None, + }), + 'history': list([ + ]), + 'id': '369afeed-61e3-4bf7-9747-66e05807134c', + 'isDue': False, + 'nextDue': list([ + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + ]), + 'notes': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.', + 'priority': 1, + 'reminders': list([ + ]), + 'repeat': dict({ + 'f': False, + 'm': False, + 's': False, + 'su': True, + 't': False, + 'th': False, + 'w': False, + }), + 'startDate': '2024-04-04T22:00:00+00:00', + 'streak': 1, + 'tags': list([ + ]), + 'text': 'Monatliche Finanzübersicht erstellen', + 'type': 'daily', + 'up': None, + 'updatedAt': '2024-04-04T22:00:00+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', + 'value': -0.9215181434950852, + 'weeksOfMonth': list([ + 0, + ]), + 'yesterDaily': True, + }), dict({ 'alias': None, 'attribute': 'str', @@ -5199,6 +5424,81 @@ ]), 'yesterDaily': True, }), + dict({ + 'alias': None, + 'attribute': 'str', + 'byHabitica': False, + 'challenge': dict({ + 'broken': None, + 'id': None, + 'shortName': None, + 'taskId': None, + 'winner': None, + }), + 'checklist': list([ + ]), + 'collapseChecklist': False, + 'completed': False, + 'counterDown': None, + 'counterUp': None, + 'createdAt': '2024-04-04T22:00:00+00:00', + 'date': None, + 'daysOfMonth': list([ + ]), + 'down': None, + 'everyX': 1, + 'frequency': 'monthly', + 'group': dict({ + 'assignedDate': None, + 'assignedUsers': list([ + ]), + 'assignedUsersDetail': dict({ + }), + 'assigningUsername': None, + 'completedBy': dict({ + 'date': None, + 'userId': None, + }), + 'id': None, + 'managerNotes': None, + 'taskId': None, + }), + 'history': list([ + ]), + 'id': '369afeed-61e3-4bf7-9747-66e05807134c', + 'isDue': False, + 'nextDue': list([ + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + ]), + 'notes': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.', + 'priority': 1, + 'reminders': list([ + ]), + 'repeat': dict({ + 'f': False, + 'm': False, + 's': False, + 'su': True, + 't': False, + 'th': False, + 'w': False, + }), + 'startDate': '2024-04-04T22:00:00+00:00', + 'streak': 1, + 'tags': list([ + ]), + 'text': 'Monatliche Finanzübersicht erstellen', + 'type': 'daily', + 'up': None, + 'updatedAt': '2024-04-04T22:00:00+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', + 'value': -0.9215181434950852, + 'weeksOfMonth': list([ + 0, + ]), + 'yesterDaily': True, + }), dict({ 'alias': None, 'attribute': 'str', diff --git a/tests/components/habitica/snapshots/test_todo.ambr b/tests/components/habitica/snapshots/test_todo.ambr index 88204d53ded..fef9404a0f0 100644 --- a/tests/components/habitica/snapshots/test_todo.ambr +++ b/tests/components/habitica/snapshots/test_todo.ambr @@ -49,6 +49,13 @@ 'summary': 'Arbeite an einem kreativen Projekt', 'uid': '6e53f1f5-a315-4edd-984d-8d762e4a08ef', }), + dict({ + 'description': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.', + 'due': '2024-12-14', + 'status': 'needs_action', + 'summary': 'Monatliche Finanzübersicht erstellen', + 'uid': '369afeed-61e3-4bf7-9747-66e05807134c', + }), dict({ 'description': 'Wähle eine Programmiersprache aus, die du noch nicht kennst, und lerne die Grundlagen.', 'status': 'needs_action', @@ -151,7 +158,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '4', + 'state': '5', }) # --- # name: test_todos[todo.test_user_to_do_s-entry] From 12c3d54a638b59d25e946a8cc61569a0e186cd27 Mon Sep 17 00:00:00 2001 From: Dionisis Toulatos <47108480+dionisis2014@users.noreply.github.com> Date: Sat, 12 Apr 2025 20:30:06 +0300 Subject: [PATCH 225/258] Fix MQTT device discovery when using node_id (#142784) * Fix device discovery when using node_id * tests --------- Co-authored-by: jbouwh Co-authored-by: Jan Bouwhuis --- homeassistant/components/mqtt/discovery.py | 2 +- tests/components/mqtt/test_discovery.py | 172 ++++++++++++++++++++- 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index a527e712615..4ebdbbb6236 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -254,7 +254,7 @@ def _generate_device_config( comp_config = config[CONF_COMPONENTS] for platform, discover_id in mqtt_data.discovery_already_discovered: ids = discover_id.split(" ") - component_node_id = ids.pop(0) + component_node_id = f"{ids.pop(1)} {ids.pop(0)}" if len(ids) > 2 else ids.pop(0) component_object_id = " ".join(ids) if not ids: continue diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index ee33cbcbaa1..ee559ef4235 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -388,23 +388,181 @@ async def test_only_valid_components( assert not mock_dispatcher_send.called -async def test_correct_config_discovery( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +@pytest.mark.parametrize( + ("discovery_topic", "discovery_hash"), + [ + ("homeassistant/binary_sensor/bla/config", ("binary_sensor", "bla")), + ("homeassistant/binary_sensor/node/bla/config", ("binary_sensor", "node bla")), + ], + ids=["without_node", "with_node"], +) +async def test_correct_config_discovery_component( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + device_registry: dr.DeviceRegistry, + discovery_topic: str, + discovery_hash: tuple[str, str], ) -> None: """Test sending in correct JSON.""" await mqtt_mock_entry() + config_init = { + "name": "Beer", + "state_topic": "test-topic", + "unique_id": "bla001", + "device": {"identifiers": "0AFFD2", "name": "test_device1"}, + "o": {"name": "foobar"}, + } async_fire_mqtt_message( hass, - "homeassistant/binary_sensor/bla/config", - '{ "name": "Beer", "state_topic": "test-topic" }', + discovery_topic, + json.dumps(config_init), ) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.beer") + state = hass.states.get("binary_sensor.test_device1_beer") assert state is not None - assert state.name == "Beer" - assert ("binary_sensor", "bla") in hass.data["mqtt"].discovery_already_discovered + assert state.name == "test_device1 Beer" + assert discovery_hash in hass.data["mqtt"].discovery_already_discovered + + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) + assert device_entry is not None + assert device_entry.name == "test_device1" + + # Update the device and component + config_update = { + "name": "Milk", + "state_topic": "test-topic", + "unique_id": "bla001", + "device": {"identifiers": "0AFFD2", "name": "test_device2"}, + "o": {"name": "foobar"}, + } + async_fire_mqtt_message( + hass, + discovery_topic, + json.dumps(config_update), + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_device1_beer") + + assert state is not None + assert state.name == "test_device2 Milk" + assert discovery_hash in hass.data["mqtt"].discovery_already_discovered + + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) + assert device_entry is not None + assert device_entry.name == "test_device2" + + # Remove the device and component + async_fire_mqtt_message( + hass, + discovery_topic, + "", + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_device1_beer") + + assert state is None + + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) + assert device_entry is None + + +@pytest.mark.parametrize( + ("discovery_topic", "discovery_hash"), + [ + ("homeassistant/device/some_id/config", ("binary_sensor", "some_id bla")), + ( + "homeassistant/device/node_id/some_id/config", + ("binary_sensor", "some_id node_id bla"), + ), + ], + ids=["without_node", "with_node"], +) +async def test_correct_config_discovery_device( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + device_registry: dr.DeviceRegistry, + discovery_topic: str, + discovery_hash: tuple[str, str], +) -> None: + """Test sending in correct JSON.""" + await mqtt_mock_entry() + config_init = { + "cmps": { + "bla": { + "platform": "binary_sensor", + "name": "Beer", + "state_topic": "test-topic", + "unique_id": "bla001", + }, + }, + "device": {"identifiers": "0AFFD2", "name": "test_device1"}, + "o": {"name": "foobar"}, + } + async_fire_mqtt_message( + hass, + discovery_topic, + json.dumps(config_init), + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_device1_beer") + + assert state is not None + assert state.name == "test_device1 Beer" + assert discovery_hash in hass.data["mqtt"].discovery_already_discovered + + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) + assert device_entry is not None + assert device_entry.name == "test_device1" + + # Update the device and component + config_update = { + "cmps": { + "bla": { + "platform": "binary_sensor", + "name": "Milk", + "state_topic": "test-topic", + "unique_id": "bla001", + }, + }, + "device": {"identifiers": "0AFFD2", "name": "test_device2"}, + "o": {"name": "foobar"}, + } + async_fire_mqtt_message( + hass, + discovery_topic, + json.dumps(config_update), + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_device1_beer") + + assert state is not None + assert state.name == "test_device2 Milk" + assert discovery_hash in hass.data["mqtt"].discovery_already_discovered + + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) + assert device_entry is not None + assert device_entry.name == "test_device2" + + # Remove the device and component + async_fire_mqtt_message( + hass, + discovery_topic, + "", + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_device1_beer") + + assert state is None + + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) + assert device_entry is None @pytest.mark.parametrize( From 34767d40582da0ad1dc73b82433103f4da9f13b4 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sat, 12 Apr 2025 16:09:17 +0100 Subject: [PATCH 226/258] Force Squeezebox item id to string (#142793) force item_id to string --- .../components/squeezebox/browse_media.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 633f004993f..eadd706fcd8 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -148,7 +148,7 @@ def _build_response_apps_radios_category( ) -> BrowseMedia: """Build item for App or radio category.""" return BrowseMedia( - media_content_id=item.get("id", ""), + media_content_id=item["id"], title=item["title"], media_content_type=cmd, media_class=browse_data.content_type_media_class[cmd]["item"], @@ -163,7 +163,7 @@ def _build_response_known_app( """Build item for app or radio.""" return BrowseMedia( - media_content_id=item.get("id", ""), + media_content_id=item["id"], title=item["title"], media_content_type=search_type, media_class=browse_data.content_type_media_class[search_type]["item"], @@ -185,7 +185,7 @@ def _build_response_favorites(item: dict[str, Any]) -> BrowseMedia: ) if item["hasitems"] and not item["isaudio"]: return BrowseMedia( - media_content_id=item.get("id", ""), + media_content_id=item["id"], title=item["title"], media_content_type="Favorites", media_class=CONTENT_TYPE_MEDIA_CLASS["Favorites"]["item"], @@ -193,7 +193,7 @@ def _build_response_favorites(item: dict[str, Any]) -> BrowseMedia: can_play=False, ) return BrowseMedia( - media_content_id=item.get("id", ""), + media_content_id=item["id"], title=item["title"], media_content_type="Favorites", media_class=CONTENT_TYPE_MEDIA_CLASS[MediaType.TRACK]["item"], @@ -217,7 +217,7 @@ def _get_item_thumbnail( item_thumbnail = player.generate_image_url_from_track_id(artwork_track_id) elif item_type is not None: item_thumbnail = entity.get_browse_image_url( - item_type, item.get("id", ""), artwork_track_id + item_type, item["id"], artwork_track_id ) elif search_type in ["Apps", "Radios"]: @@ -263,6 +263,8 @@ async def build_item_response( children = [] 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": child_media = _build_response_favorites(item) @@ -294,7 +296,7 @@ async def build_item_response( elif item_type: child_media = BrowseMedia( - media_content_id=str(item.get("id", "")), + media_content_id=item["id"], title=item["title"], media_content_type=item_type, media_class=CONTENT_TYPE_MEDIA_CLASS[item_type]["item"], From 88eef379b23b408fab91af1acb2f9c18cd336d11 Mon Sep 17 00:00:00 2001 From: Eric Park Date: Wed, 16 Apr 2025 16:41:32 -0400 Subject: [PATCH 227/258] Keep track of last play status update time in Apple TV (#142838) --- homeassistant/components/apple_tv/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index b68d74e6115..b6d451a9ea0 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -120,6 +120,7 @@ class AppleTvMediaPlayer( """Initialize the Apple TV media player.""" super().__init__(name, identifier, manager) self._playing: Playing | None = None + self._playing_last_updated: datetime | None = None self._app_list: dict[str, str] = {} @callback @@ -209,6 +210,7 @@ class AppleTvMediaPlayer( This is a callback function from pyatv.interface.PushListener. """ self._playing = playstatus + self._playing_last_updated = dt_util.utcnow() self.async_write_ha_state() @callback @@ -316,7 +318,7 @@ class AppleTvMediaPlayer( def media_position_updated_at(self) -> datetime | None: """Last valid time of media position.""" if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}: - return dt_util.utcnow() + return self._playing_last_updated return None async def async_play_media( From c341b86520d2912f76d393c522296fd2b2a2127e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 14 Apr 2025 20:12:34 +0200 Subject: [PATCH 228/258] Select correct Reolink device uid (#142864) * Select correct device_uid * Fix styling * restructure * Add test * Update test_util.py * Add explanation string --- homeassistant/components/reolink/__init__.py | 16 ++++++++ homeassistant/components/reolink/util.py | 12 ++++-- tests/components/reolink/conftest.py | 1 + tests/components/reolink/test_util.py | 41 +++++++++++++++++++- 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index c326f1120c9..f7d13c1d90f 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -371,6 +371,9 @@ def migrate_entity_ids( 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) @@ -383,6 +386,9 @@ def migrate_entity_ids( 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: @@ -415,6 +421,11 @@ def migrate_entity_ids( host.unique_id ): new_id = f"{host.unique_id}_{entity.unique_id.split('_', 1)[1]}" + _LOGGER.debug( + "Updating Reolink entity unique_id from %s to %s", + entity.unique_id, + new_id, + ) entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id) if entity.device_id in ch_device_ids: @@ -430,6 +441,11 @@ def migrate_entity_ids( continue if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch): new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}" + _LOGGER.debug( + "Updating Reolink entity unique_id from %s to %s", + entity.unique_id, + new_id, + ) existing_entity = entity_reg.async_get_entity_id( entity.domain, entity.platform, new_id ) diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index 12b4825caeb..17e666ac52c 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -79,11 +79,15 @@ def get_device_uid_and_ch( device: dr.DeviceEntry, host: ReolinkHost ) -> tuple[list[str], int | None, bool]: """Get the channel and the split device_uid from a reolink DeviceEntry.""" - device_uid = [ - dev_id[1].split("_") for dev_id in device.identifiers if dev_id[0] == DOMAIN - ][0] - + device_uid = [] is_chime = False + + for dev_id in device.identifiers: + if dev_id[0] == DOMAIN: + device_uid = dev_id[1].split("_") + if device_uid[0] == host.unique_id: + break + if len(device_uid) < 2: # NVR itself ch = None diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 21acced3d1d..3bd1539fc36 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -77,6 +77,7 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.check_new_firmware.return_value = False host_mock.unsubscribe.return_value = True host_mock.logout.return_value = True + host_mock.is_nvr = True host_mock.is_hub = False host_mock.mac_address = TEST_MAC host_mock.uid = TEST_UID diff --git a/tests/components/reolink/test_util.py b/tests/components/reolink/test_util.py index ef66d471801..181249b8bff 100644 --- a/tests/components/reolink/test_util.py +++ b/tests/components/reolink/test_util.py @@ -23,15 +23,21 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) +from homeassistant.components.reolink.const import DOMAIN +from homeassistant.components.reolink.util import get_device_uid_and_ch from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import device_registry as dr -from .conftest import TEST_NVR_NAME +from .conftest import TEST_NVR_NAME, TEST_UID, TEST_UID_CAM from tests.common import MockConfigEntry +DEV_ID_NVR = f"{TEST_UID}_{TEST_UID_CAM}" +DEV_ID_STANDALONE_CAM = f"{TEST_UID_CAM}" + @pytest.mark.parametrize( ("side_effect", "expected"), @@ -123,3 +129,36 @@ async def test_try_function( assert err.value.translation_key == expected.translation_key reolink_connect.set_volume.reset_mock(side_effect=True) + + +@pytest.mark.parametrize( + ("identifiers"), + [ + ({(DOMAIN, DEV_ID_NVR), (DOMAIN, DEV_ID_STANDALONE_CAM)}), + ({(DOMAIN, DEV_ID_STANDALONE_CAM), (DOMAIN, DEV_ID_NVR)}), + ], +) +async def test_get_device_uid_and_ch( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + device_registry: dr.DeviceRegistry, + identifiers: set[tuple[str, str]], +) -> None: + """Test get_device_uid_and_ch with multiple identifiers.""" + reolink_connect.channels = [0] + + dev_entry = device_registry.async_get_or_create( + identifiers=identifiers, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = get_device_uid_and_ch(dev_entry, config_entry.runtime_data.host) + # always get the uid and channel form the DEV_ID_NVR since is_nvr = True + assert result == ([TEST_UID, TEST_UID_CAM], 0, False) From 389297155dab9aad88b95960f5159fe9101544ef Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 14 Apr 2025 09:39:41 +0200 Subject: [PATCH 229/258] Fix Reolink Home Hub Pro playback (#142871) Fix Home Hub Pro playback --- homeassistant/components/reolink/media_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 39514d58cb7..092f0d4ddca 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -70,7 +70,7 @@ class ReolinkVODMediaSource(MediaSource): host = get_host(self.hass, config_entry_id) def get_vod_type() -> VodRequestType: - if filename.endswith((".mp4", ".vref")): + if filename.endswith((".mp4", ".vref")) or host.api.is_hub: if host.api.is_nvr: return VodRequestType.DOWNLOAD return VodRequestType.PLAYBACK From 1e69ce9111cd5264c5bbe5d50fe9a6e5e2d15c82 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 14 Apr 2025 01:05:34 -0700 Subject: [PATCH 230/258] Fix quality loss for LLM conversation agent question answering (#142873) * Fix a bug parsing a streaming response with no json * Remove debug lines * Fix quality loss for LLM conversation agent question answering * Update tests --- homeassistant/helpers/llm.py | 32 ++++++++++++++++++++++++++------ tests/helpers/test_llm.py | 28 ++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 7f6fe22ec70..df94788a981 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -71,6 +71,19 @@ NO_ENTITIES_PROMPT = ( "to their voice assistant in Home Assistant." ) +DYNAMIC_CONTEXT_PROMPT = """You ARE equipped to answer questions about the current state of +the home using the `GetLiveContext` tool. This is a primary function. Do not state you lack the +functionality if the question requires live data. +If the user asks about device existence/type (e.g., "Do I have lights in the bedroom?"): Answer +from the static context below. +If the user asks about the CURRENT state, value, or mode (e.g., "Is the lock locked?", +"Is the fan on?", "What mode is the thermostat in?", "What is the temperature outside?"): + 1. Recognize this requires live data. + 2. You MUST call `GetLiveContext`. This tool will provide the needed real-time information (like temperature from the local weather, lock status, etc.). + 3. Use the tool's response** to answer the user accurately (e.g., "The temperature outside is [value from tool]."). +For general knowledge questions not about the home: Answer truthfully from internal knowledge. +""" + @callback def async_render_no_api_prompt(hass: HomeAssistant) -> str: @@ -384,6 +397,8 @@ class AssistAPI(API): ): prompt.append("This device is not able to start timers.") + prompt.append(DYNAMIC_CONTEXT_PROMPT) + return prompt @callback @@ -395,7 +410,7 @@ class AssistAPI(API): if exposed_entities and exposed_entities["entities"]: prompt.append( - "An overview of the areas and the devices in this smart home:" + "Static Context: An overview of the areas and the devices in this smart home:" ) prompt.append(yaml_util.dump(list(exposed_entities["entities"].values()))) @@ -457,7 +472,7 @@ class AssistAPI(API): ) if exposed_domains: - tools.append(GetHomeStateTool()) + tools.append(GetLiveContextTool()) return tools @@ -898,7 +913,7 @@ class CalendarGetEventsTool(Tool): return {"success": True, "result": events} -class GetHomeStateTool(Tool): +class GetLiveContextTool(Tool): """Tool for getting the current state of exposed entities. This returns state for all entities that have been exposed to @@ -906,8 +921,13 @@ class GetHomeStateTool(Tool): returns state for entities based on intent parameters. """ - name = "get_home_state" - description = "Get the current state of all devices in the home. " + name = "GetLiveContext" + description = ( + "Use this tool when the user asks a question about the CURRENT state, " + "value, or mode of a specific device, sensor, entity, or area in the " + "smart home, and the answer can be improved with real-time data not " + "available in the static device overview list. " + ) async def async_call( self, @@ -925,7 +945,7 @@ class GetHomeStateTool(Tool): if not exposed_entities["entities"]: return {"success": False, "error": NO_ENTITIES_PROMPT} prompt = [ - "An overview of the areas and the devices in this smart home:", + "Live Context: An overview of the areas and the devices in this smart home:", yaml_util.dump(list(exposed_entities["entities"].values())), ] return { diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 19ada407550..42dd4da6197 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -181,13 +181,13 @@ async def test_assist_api( assert len(llm.async_get_apis(hass)) == 1 api = await llm.async_get_api(hass, "assist", llm_context) - assert [tool.name for tool in api.tools] == ["get_home_state"] + assert [tool.name for tool in api.tools] == ["GetLiveContext"] # Match all intent_handler.platforms = None api = await llm.async_get_api(hass, "assist", llm_context) - assert [tool.name for tool in api.tools] == ["test_intent", "get_home_state"] + assert [tool.name for tool in api.tools] == ["test_intent", "GetLiveContext"] # Match specific domain intent_handler.platforms = {"light"} @@ -575,7 +575,7 @@ async def test_assist_api_prompt( suggested_area="Test Area 2", ) ) - exposed_entities_prompt = """An overview of the areas and the devices in this smart home: + exposed_entities_prompt = """Live Context: An overview of the areas and the devices in this smart home: - names: Kitchen domain: light state: 'on' @@ -623,7 +623,7 @@ async def test_assist_api_prompt( state: unavailable areas: Test Area 2 """ - stateless_exposed_entities_prompt = """An overview of the areas and the devices in this smart home: + stateless_exposed_entities_prompt = """Static Context: An overview of the areas and the devices in this smart home: - names: Kitchen domain: light - names: Living Room @@ -669,17 +669,30 @@ async def test_assist_api_prompt( "When a user asks to turn on all devices of a specific type, " "ask user to specify an area, unless there is only one device of that type." ) + dynamic_context_prompt = """You ARE equipped to answer questions about the current state of +the home using the `GetLiveContext` tool. This is a primary function. Do not state you lack the +functionality if the question requires live data. +If the user asks about device existence/type (e.g., "Do I have lights in the bedroom?"): Answer +from the static context below. +If the user asks about the CURRENT state, value, or mode (e.g., "Is the lock locked?", +"Is the fan on?", "What mode is the thermostat in?", "What is the temperature outside?"): + 1. Recognize this requires live data. + 2. You MUST call `GetLiveContext`. This tool will provide the needed real-time information (like temperature from the local weather, lock status, etc.). + 3. Use the tool's response** to answer the user accurately (e.g., "The temperature outside is [value from tool]."). +For general knowledge questions not about the home: Answer truthfully from internal knowledge. +""" api = await llm.async_get_api(hass, "assist", llm_context) assert api.api_prompt == ( f"""{first_part_prompt} {area_prompt} {no_timer_prompt} +{dynamic_context_prompt} {stateless_exposed_entities_prompt}""" ) - # Verify that the get_home_state tool returns the same results as the exposed_entities_prompt + # Verify that the GetLiveContext tool returns the same results as the exposed_entities_prompt result = await api.async_call_tool( - llm.ToolInput(tool_name="get_home_state", tool_args={}) + llm.ToolInput(tool_name="GetLiveContext", tool_args={}) ) assert result == { "success": True, @@ -697,6 +710,7 @@ async def test_assist_api_prompt( f"""{first_part_prompt} {area_prompt} {no_timer_prompt} +{dynamic_context_prompt} {stateless_exposed_entities_prompt}""" ) @@ -712,6 +726,7 @@ async def test_assist_api_prompt( f"""{first_part_prompt} {area_prompt} {no_timer_prompt} +{dynamic_context_prompt} {stateless_exposed_entities_prompt}""" ) @@ -723,6 +738,7 @@ async def test_assist_api_prompt( assert api.api_prompt == ( f"""{first_part_prompt} {area_prompt} +{dynamic_context_prompt} {stateless_exposed_entities_prompt}""" ) From 9f1a830d322c998b0a745531edd3a06be8849a84 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 14 Apr 2025 09:39:01 +0200 Subject: [PATCH 231/258] Only get tracked pairs for kraken (#142877) Only get tracked pairs Getting all available pairs leads to a too long request URL --- homeassistant/components/kraken/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index 9a90e77f2b6..c981f3fd438 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -145,7 +145,10 @@ class KrakenData: await asyncio.sleep(CALL_RATE_LIMIT_SLEEP) def _get_websocket_name_asset_pairs(self) -> str: - return ",".join(wsname for wsname in self.tradable_asset_pairs.values()) + return ",".join( + self.tradable_asset_pairs[tracked_pair] + for tracked_pair in self._config_entry.options[CONF_TRACKED_ASSET_PAIRS] + ) def set_update_interval(self, update_interval: int) -> None: """Set the coordinator update_interval to the supplied update_interval.""" From c236cd070cc5bd119f11e685a90d14dddc2142c7 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Mon, 14 Apr 2025 00:39:50 -0400 Subject: [PATCH 232/258] Bump Environment Canada library to 0.10.1 (#142882) --- .../components/environment_canada/config_flow.py | 2 +- .../components/environment_canada/coordinator.py | 4 ++-- .../components/environment_canada/manifest.json | 2 +- .../components/environment_canada/sensor.py | 12 ++++++------ .../components/environment_canada/weather.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/environment_canada/__init__.py | 2 +- tests/components/environment_canada/conftest.py | 7 +++++-- .../environment_canada/test_config_flow.py | 2 +- .../environment_canada/test_diagnostics.py | 6 ------ 11 files changed, 20 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py index c4fd16f9522..debe1c5ae43 100644 --- a/homeassistant/components/environment_canada/config_flow.py +++ b/homeassistant/components/environment_canada/config_flow.py @@ -35,7 +35,7 @@ async def validate_input(data): lon = weather_data.lon return { - CONF_TITLE: weather_data.metadata.get("location"), + CONF_TITLE: weather_data.metadata.location, CONF_STATION: weather_data.station_id, CONF_LATITUDE: lat, CONF_LONGITUDE: lon, diff --git a/homeassistant/components/environment_canada/coordinator.py b/homeassistant/components/environment_canada/coordinator.py index e31e847cd2d..89fc92b462e 100644 --- a/homeassistant/components/environment_canada/coordinator.py +++ b/homeassistant/components/environment_canada/coordinator.py @@ -7,7 +7,7 @@ from datetime import timedelta import logging import xml.etree.ElementTree as ET -from env_canada import ECAirQuality, ECRadar, ECWeather, ec_exc +from env_canada import ECAirQuality, ECRadar, ECWeather, ECWeatherUpdateFailed, ec_exc from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -65,6 +65,6 @@ class ECDataUpdateCoordinator[DataT: ECDataType](DataUpdateCoordinator[DataT]): """Fetch data from EC.""" try: await self.ec_data.update() - except (ET.ParseError, ec_exc.UnknownStationId) as ex: + except (ET.ParseError, ECWeatherUpdateFailed, ec_exc.UnknownStationId) as ex: raise UpdateFailed(f"Error fetching {self.name} data: {ex}") from ex return self.ec_data diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index fc05e093b33..098f231a40f 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.8.0"] + "requirements": ["env-canada==0.10.1"] } diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 1685888d2bc..d27da132a35 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -145,7 +145,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="timestamp", translation_key="timestamp", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: data.metadata.get("timestamp"), + value_fn=lambda data: data.metadata.timestamp, ), ECSensorEntityDescription( key="uv_index", @@ -289,7 +289,7 @@ class ECBaseSensorEntity[DataT: ECDataType]( super().__init__(coordinator) self.entity_description = description self._ec_data = coordinator.ec_data - self._attr_attribution = self._ec_data.metadata["attribution"] + self._attr_attribution = self._ec_data.metadata.attribution self._attr_unique_id = f"{coordinator.config_entry.title}-{description.key}" self._attr_device_info = coordinator.device_info @@ -313,8 +313,8 @@ class ECSensorEntity[DataT: ECDataType](ECBaseSensorEntity[DataT]): """Initialize the sensor.""" super().__init__(coordinator, description) self._attr_extra_state_attributes = { - ATTR_LOCATION: self._ec_data.metadata.get("location"), - ATTR_STATION: self._ec_data.metadata.get("station"), + ATTR_LOCATION: self._ec_data.metadata.location, + ATTR_STATION: self._ec_data.metadata.station, } @@ -329,8 +329,8 @@ class ECAlertSensorEntity(ECBaseSensorEntity[ECWeather]): return None extra_state_attrs = { - ATTR_LOCATION: self._ec_data.metadata.get("location"), - ATTR_STATION: self._ec_data.metadata.get("station"), + ATTR_LOCATION: self._ec_data.metadata.location, + ATTR_STATION: self._ec_data.metadata.station, } for index, alert in enumerate(value, start=1): extra_state_attrs[f"alert_{index}"] = alert.get("title") diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index dd7632032ec..a5acb224bd0 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -115,7 +115,7 @@ class ECWeatherEntity( """Initialize Environment Canada weather.""" super().__init__(coordinator) self.ec_data = coordinator.ec_data - self._attr_attribution = self.ec_data.metadata["attribution"] + self._attr_attribution = self.ec_data.metadata.attribution self._attr_translation_key = "forecast" self._attr_unique_id = _calculate_unique_id( coordinator.config_entry.unique_id, False diff --git a/requirements_all.txt b/requirements_all.txt index ef0d477eda3..99a110eaa32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.8.0 +env-canada==0.10.1 # homeassistant.components.season ephem==4.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac142674d20..63ae223644c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -741,7 +741,7 @@ energyzero==2.1.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.8.0 +env-canada==0.10.1 # homeassistant.components.season ephem==4.1.6 diff --git a/tests/components/environment_canada/__init__.py b/tests/components/environment_canada/__init__.py index edc7a92a12f..61ec97ef794 100644 --- a/tests/components/environment_canada/__init__.py +++ b/tests/components/environment_canada/__init__.py @@ -33,7 +33,7 @@ async def init_integration(hass: HomeAssistant, ec_data) -> MockConfigEntry: config_entry.add_to_hass(hass) weather_mock = mock_ec() - ec_data["metadata"]["timestamp"] = datetime(2022, 10, 4, tzinfo=UTC) + ec_data["metadata"].timestamp = datetime(2022, 10, 4, tzinfo=UTC) weather_mock.conditions = ec_data["conditions"] weather_mock.alerts = ec_data["alerts"] weather_mock.daily_forecasts = ec_data["daily_forecasts"] diff --git a/tests/components/environment_canada/conftest.py b/tests/components/environment_canada/conftest.py index 19180052c93..3c7683ad0eb 100644 --- a/tests/components/environment_canada/conftest.py +++ b/tests/components/environment_canada/conftest.py @@ -4,6 +4,7 @@ import contextlib from datetime import datetime import json +from env_canada.ec_weather import MetaData import pytest from tests.common import load_fixture @@ -13,7 +14,7 @@ from tests.common import load_fixture def ec_data(): """Load Environment Canada data.""" - def date_hook(weather): + def data_hook(weather): """Convert timestamp string to datetime.""" if t := weather.get("timestamp"): @@ -22,9 +23,11 @@ def ec_data(): elif t := weather.get("period"): with contextlib.suppress(ValueError): weather["period"] = datetime.fromisoformat(t) + if t := weather.get("metadata"): + weather["metadata"] = MetaData(**t) return weather return json.loads( load_fixture("environment_canada/current_conditions_data.json"), - object_hook=date_hook, + object_hook=data_hook, ) diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index d61966e8da1..9f3fdbd43dc 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -30,7 +30,7 @@ def mocked_ec(): ec_mock.lat = FAKE_CONFIG[CONF_LATITUDE] ec_mock.lon = FAKE_CONFIG[CONF_LONGITUDE] ec_mock.language = FAKE_CONFIG[CONF_LANGUAGE] - ec_mock.metadata = {"location": FAKE_TITLE} + ec_mock.metadata.location = FAKE_TITLE ec_mock.update = AsyncMock() diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index 79b72961124..7c35c33f93a 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -1,6 +1,5 @@ """Test Environment Canada diagnostics.""" -import json from typing import Any from syrupy import SnapshotAssertion @@ -11,7 +10,6 @@ from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -31,10 +29,6 @@ async def test_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" - ec_data = json.loads( - load_fixture("environment_canada/current_conditions_data.json") - ) - config_entry = await init_integration(hass, ec_data) diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, config_entry From 9886db5d6d1c0b6fab8fce921fd4bc52f7d60c72 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 14 Apr 2025 21:24:52 +0200 Subject: [PATCH 233/258] Bump devolo_plc_api to 1.5.1 (#142908) --- homeassistant/components/devolo_home_network/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index 9b1e181d7c0..31f3a51ebeb 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["devolo_plc_api"], - "requirements": ["devolo-plc-api==1.4.1"], + "requirements": ["devolo-plc-api==1.5.1"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 99a110eaa32..9de4a6ad73a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -781,7 +781,7 @@ devialet==1.5.7 devolo-home-control-api==0.18.3 # homeassistant.components.devolo_home_network -devolo-plc-api==1.4.1 +devolo-plc-api==1.5.1 # homeassistant.components.chacon_dio dio-chacon-wifi-api==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63ae223644c..4b2163854d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -672,7 +672,7 @@ devialet==1.5.7 devolo-home-control-api==0.18.3 # homeassistant.components.devolo_home_network -devolo-plc-api==1.4.1 +devolo-plc-api==1.5.1 # homeassistant.components.chacon_dio dio-chacon-wifi-api==1.2.1 From 71f658b560a49e34071081396c4fdb717ca2d03c Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 14 Apr 2025 13:15:46 +0300 Subject: [PATCH 234/258] Don't do I/O while getting Jewish calendar data schema (#142919) --- .../components/jewish_calendar/config_flow.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/jewish_calendar/config_flow.py b/homeassistant/components/jewish_calendar/config_flow.py index 23bcb23435b..3cec9e9e24e 100644 --- a/homeassistant/components/jewish_calendar/config_flow.py +++ b/homeassistant/components/jewish_calendar/config_flow.py @@ -61,11 +61,14 @@ OPTIONS_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -def _get_data_schema(hass: HomeAssistant) -> vol.Schema: +async def _get_data_schema(hass: HomeAssistant) -> vol.Schema: default_location = { CONF_LATITUDE: hass.config.latitude, CONF_LONGITUDE: hass.config.longitude, } + get_timezones: list[str] = list( + await hass.async_add_executor_job(zoneinfo.available_timezones) + ) return vol.Schema( { vol.Required(CONF_DIASPORA, default=DEFAULT_DIASPORA): BooleanSelector(), @@ -75,9 +78,7 @@ def _get_data_schema(hass: HomeAssistant) -> vol.Schema: vol.Optional(CONF_LOCATION, default=default_location): LocationSelector(), vol.Optional(CONF_ELEVATION, default=hass.config.elevation): int, vol.Optional(CONF_TIME_ZONE, default=hass.config.time_zone): SelectSelector( - SelectSelectorConfig( - options=sorted(zoneinfo.available_timezones()), - ) + SelectSelectorConfig(options=get_timezones, sort=True) ), } ) @@ -109,7 +110,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=self.add_suggested_values_to_schema( - _get_data_schema(self.hass), user_input + await _get_data_schema(self.hass), user_input ), ) @@ -121,7 +122,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN): if not user_input: return self.async_show_form( data_schema=self.add_suggested_values_to_schema( - _get_data_schema(self.hass), + await _get_data_schema(self.hass), reconfigure_entry.data, ), step_id="reconfigure", From 7edcddd3e49193ef962b9ce52f4ecdfaa789c81a Mon Sep 17 00:00:00 2001 From: Alex L <9060360+AlexLamond@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:22:53 +0200 Subject: [PATCH 235/258] Update UK Transport Integration URL (#142949) --- homeassistant/components/uk_transport/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 594d46c74ab..a52750282df 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -163,7 +163,7 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): self._destination_re = re.compile(f"{bus_direction}", re.IGNORECASE) sensor_name = f"Next bus to {bus_direction}" - stop_url = f"bus/stop/{stop_atcocode}/live.json" + stop_url = f"bus/stop/{stop_atcocode}.json" UkTransportSensor.__init__(self, sensor_name, api_app_id, api_app_key, stop_url) self.update = Throttle(interval)(self._update) @@ -226,7 +226,7 @@ class UkTransportLiveTrainTimeSensor(UkTransportSensor): self._next_trains = [] sensor_name = f"Next train to {calling_at}" - query_url = f"train/station/{station_code}/live.json" + query_url = f"train/station/{station_code}.json" UkTransportSensor.__init__( self, sensor_name, api_app_id, api_app_key, query_url From 2d149dc7462648390be3f39dd34e967270c2d8f2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 14 Apr 2025 22:52:21 +0200 Subject: [PATCH 236/258] Bump holidays to 0.70 (#142954) --- 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 4c73210c36e..d54d6955087 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.69", "babel==2.15.0"] + "requirements": ["holidays==0.70", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index b08a5ed9fff..60196fb15b7 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.69"] + "requirements": ["holidays==0.70"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9de4a6ad73a..697ea705ffb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1154,7 +1154,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.69 +holidays==0.70 # homeassistant.components.frontend home-assistant-frontend==20250411.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b2163854d8..e48d0daa66e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -981,7 +981,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.69 +holidays==0.70 # homeassistant.components.frontend home-assistant-frontend==20250411.0 From 7c867852a9c00400cea5c1969fcc017639b84247 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 15 Apr 2025 09:55:16 +0200 Subject: [PATCH 237/258] Fix switch state for Comelit (#142978) --- homeassistant/components/comelit/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/comelit/switch.py b/homeassistant/components/comelit/switch.py index 2c751cbe2cb..98f0894ca30 100644 --- a/homeassistant/components/comelit/switch.py +++ b/homeassistant/components/comelit/switch.py @@ -81,4 +81,7 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity): @property def is_on(self) -> bool: """Return True if switch is on.""" - return self.coordinator.data[OTHER][self._device.index].status == STATE_ON + return ( + self.coordinator.data[self._device.type][self._device.index].status + == STATE_ON + ) From e240707b32ea12948b46373a2cdb495c4ed45f6a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 15 Apr 2025 15:03:47 +0200 Subject: [PATCH 238/258] Bump reolink-aio to 0.13.2 (#142985) --- 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 9105dfda66f..59a2741571f 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.1"] + "requirements": ["reolink-aio==0.13.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 697ea705ffb..0f6f6cf8b25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2627,7 +2627,7 @@ renault-api==0.2.9 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.1 +reolink-aio==0.13.2 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e48d0daa66e..8261be65f11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2128,7 +2128,7 @@ renault-api==0.2.9 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.1 +reolink-aio==0.13.2 # homeassistant.components.rflink rflink==0.0.66 From 74c4553bb03ecfcb894972a702457aefc92fd490 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 16 Apr 2025 14:02:27 +0200 Subject: [PATCH 239/258] Increase uptime deviation for Shelly (#142996) * Increase uptime deviation for Shelly * fix test * make troubleshooting easy * change deviation interval * increase deviation to 1m --- homeassistant/components/shelly/const.py | 2 +- homeassistant/components/shelly/utils.py | 12 +++++++++++- tests/components/shelly/test_utils.py | 6 ++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 0c64df52409..cc3ec564b3f 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -209,7 +209,7 @@ KELVIN_MIN_VALUE_COLOR: Final = 3000 BLOCK_WRONG_SLEEP_PERIOD = 21600 BLOCK_EXPECTED_SLEEP_PERIOD = 43200 -UPTIME_DEVIATION: Final = 5 +UPTIME_DEVIATION: Final = 60 # Time to wait before reloading entry upon device config change ENTRY_RELOAD_COOLDOWN = 60 diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a5e08faf0e0..9284afdd567 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -200,8 +200,18 @@ def get_device_uptime(uptime: float, last_uptime: datetime | None) -> datetime: if ( not last_uptime - or abs((delta_uptime - last_uptime).total_seconds()) > UPTIME_DEVIATION + or (diff := abs((delta_uptime - last_uptime).total_seconds())) + > UPTIME_DEVIATION ): + if last_uptime: + LOGGER.debug( + "Time deviation %s > %s: uptime=%s, last_uptime=%s, delta_uptime=%s", + diff, + UPTIME_DEVIATION, + uptime, + last_uptime, + delta_uptime, + ) return delta_uptime return last_uptime diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index b7c3dff10f6..ae3caa93825 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -21,6 +21,7 @@ from homeassistant.components.shelly.const import ( GEN1_RELEASE_URL, GEN2_BETA_RELEASE_URL, GEN2_RELEASE_URL, + UPTIME_DEVIATION, ) from homeassistant.components.shelly.utils import ( get_block_channel_name, @@ -188,8 +189,9 @@ async def test_get_device_uptime() -> None: ) == dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:00+00:00")) assert get_device_uptime( - 50, dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:00+00:00")) - ) == dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:10+00:00")) + 55 - UPTIME_DEVIATION, + dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:00+00:00")), + ) == dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:43:05+00:00")) async def test_get_block_input_triggers( From 63be0e2e1ad5c12b3fc18d0d256a489dac71b51e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 19 Apr 2025 10:45:09 +0200 Subject: [PATCH 240/258] Bump pysmhi to 1.0.2 (#143007) Co-authored-by: Franck Nijhof --- homeassistant/components/smhi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 89443fc7e27..0af692b800c 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/smhi", "iot_class": "cloud_polling", "loggers": ["pysmhi"], - "requirements": ["pysmhi==1.0.1"] + "requirements": ["pysmhi==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0f6f6cf8b25..06f76e0acf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2325,7 +2325,7 @@ pysmartthings==3.0.4 pysmarty2==0.10.2 # homeassistant.components.smhi -pysmhi==1.0.1 +pysmhi==1.0.2 # homeassistant.components.edl21 pysml==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8261be65f11..c1875304828 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1895,7 +1895,7 @@ pysmartthings==3.0.4 pysmarty2==0.10.2 # homeassistant.components.smhi -pysmhi==1.0.1 +pysmhi==1.0.2 # homeassistant.components.edl21 pysml==0.0.12 From 80ef32f09d26a4b43d608f71edf1365411ca0440 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 16 Apr 2025 00:27:07 +0200 Subject: [PATCH 241/258] Add Python-2.0 to list of approved licenses (#143052) --- script/licenses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/licenses.py b/script/licenses.py index 62e1845b911..ab8ab62eb1d 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -88,6 +88,7 @@ OSI_APPROVED_LICENSES_SPDX = { "MPL-1.1", "MPL-2.0", "PSF-2.0", + "Python-2.0", "Unlicense", "Zlib", "ZPL-2.1", From 2f99164781ccc9ffe7548be07a8bad3336798116 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Apr 2025 12:23:54 +0200 Subject: [PATCH 242/258] Reduce jumping Starlink uptime sensor (#143076) --- homeassistant/components/starlink/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py index d07e8174b27..14cbf6fe876 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -113,7 +113,9 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( translation_key="last_boot_time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: now() - timedelta(seconds=data.status["uptime"]), + value_fn=lambda data: ( + now() - timedelta(seconds=data.status["uptime"]) + ).replace(microsecond=0), ), StarlinkSensorEntityDescription( key="ping_drop_rate", From 9d1ff37a79b4ab17991f7db651ddde33cd2fcaee Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 19 Apr 2025 04:38:34 -0400 Subject: [PATCH 243/258] Bump ZHA to 0.0.56 (#143165) Co-authored-by: Franck Nijhof --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 1c2d6556271..04f3658d924 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.55"], + "requirements": ["zha==0.0.56"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 06f76e0acf9..81284e09591 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3152,7 +3152,7 @@ zeroconf==0.146.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.55 +zha==0.0.56 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1875304828..c2fc32e6503 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2542,7 +2542,7 @@ zeroconf==0.146.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.55 +zha==0.0.56 # homeassistant.components.zwave_js zwave-js-server-python==0.62.0 From 2a74deb84e1af29c3d72a0ccd8a757b582402e9a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 19 Apr 2025 09:53:31 +0200 Subject: [PATCH 244/258] Fix SmartThings soundbar without media playback (#143170) --- .../components/smartthings/media_player.py | 44 ++-- .../components/smartthings/sensor.py | 8 +- .../components/smartthings/switch.py | 1 - tests/components/smartthings/conftest.py | 1 + .../device_status/vd_network_audio_003s.json | 231 ++++++++++++++++++ .../devices/vd_network_audio_003s.json | 115 +++++++++ .../smartthings/snapshots/test_init.ambr | 33 +++ .../snapshots/test_media_player.ambr | 50 ++++ 8 files changed, 455 insertions(+), 28 deletions(-) create mode 100644 tests/components/smartthings/fixtures/device_status/vd_network_audio_003s.json create mode 100644 tests/components/smartthings/fixtures/devices/vd_network_audio_003s.json diff --git a/homeassistant/components/smartthings/media_player.py b/homeassistant/components/smartthings/media_player.py index 9a676d2efb6..335e8255ae4 100644 --- a/homeassistant/components/smartthings/media_player.py +++ b/homeassistant/components/smartthings/media_player.py @@ -23,7 +23,6 @@ from .entity import SmartThingsEntity MEDIA_PLAYER_CAPABILITIES = ( Capability.AUDIO_MUTE, Capability.AUDIO_VOLUME, - Capability.MEDIA_PLAYBACK, ) CONTROLLABLE_SOURCES = ["bluetooth", "wifi"] @@ -100,27 +99,25 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity): ) def _determine_features(self) -> MediaPlayerEntityFeature: - flags = MediaPlayerEntityFeature(0) - playback_commands = self.get_attribute_value( - Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS + flags = ( + MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.VOLUME_MUTE ) - if "play" in playback_commands: - flags |= MediaPlayerEntityFeature.PLAY - if "pause" in playback_commands: - flags |= MediaPlayerEntityFeature.PAUSE - if "stop" in playback_commands: - flags |= MediaPlayerEntityFeature.STOP - if "rewind" in playback_commands: - flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK - if "fastForward" in playback_commands: - flags |= MediaPlayerEntityFeature.NEXT_TRACK - if self.supports_capability(Capability.AUDIO_VOLUME): - flags |= ( - MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.VOLUME_STEP + if self.supports_capability(Capability.MEDIA_PLAYBACK): + playback_commands = self.get_attribute_value( + Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS ) - if self.supports_capability(Capability.AUDIO_MUTE): - flags |= MediaPlayerEntityFeature.VOLUME_MUTE + if "play" in playback_commands: + flags |= MediaPlayerEntityFeature.PLAY + if "pause" in playback_commands: + flags |= MediaPlayerEntityFeature.PAUSE + if "stop" in playback_commands: + flags |= MediaPlayerEntityFeature.STOP + if "rewind" in playback_commands: + flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK + if "fastForward" in playback_commands: + flags |= MediaPlayerEntityFeature.NEXT_TRACK if self.supports_capability(Capability.SWITCH): flags |= ( MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF @@ -270,6 +267,13 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity): def state(self) -> MediaPlayerState | None: """State of the media player.""" if self.supports_capability(Capability.SWITCH): + if not self.supports_capability(Capability.MEDIA_PLAYBACK): + if ( + self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) + == "on" + ): + return MediaPlayerState.ON + return MediaPlayerState.OFF if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on": if ( self.source is not None diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index e081f35d0e0..d5a465b8ccc 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -194,13 +194,7 @@ CAPABILITY_TO_SENSORS: dict[ native_unit_of_measurement=PERCENTAGE, deprecated=( lambda status: "media_player" - if all( - capability in status - for capability in ( - Capability.AUDIO_MUTE, - Capability.MEDIA_PLAYBACK, - ) - ) + if Capability.AUDIO_MUTE in status else None ), ) diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 4e62957d3d4..ff53082ac7c 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -38,7 +38,6 @@ AC_CAPABILITIES = ( MEDIA_PLAYER_CAPABILITIES = ( Capability.AUDIO_MUTE, Capability.AUDIO_VOLUME, - Capability.MEDIA_PLAYBACK, ) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 26af812fe1f..536c1275ef7 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -108,6 +108,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "da_ref_normal_000001", "da_ref_normal_01011", "vd_network_audio_002s", + "vd_network_audio_003s", "vd_sensor_light_2023", "iphone", "da_sac_ehs_000001_sub", diff --git a/tests/components/smartthings/fixtures/device_status/vd_network_audio_003s.json b/tests/components/smartthings/fixtures/device_status/vd_network_audio_003s.json new file mode 100644 index 00000000000..e635f6c793a --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/vd_network_audio_003s.json @@ -0,0 +1,231 @@ +{ + "components": { + "main": { + "samsungvd.soundFrom": { + "mode": { + "value": 29, + "timestamp": "2025-04-05T13:51:47.865Z" + }, + "detailName": { + "value": "None", + "timestamp": "2025-04-05T13:51:50.230Z" + } + }, + "audioVolume": { + "volume": { + "value": 6, + "unit": "%", + "timestamp": "2025-04-17T11:17:25.272Z" + } + }, + "samsungvd.audioGroupInfo": { + "role": { + "value": null + }, + "channel": { + "value": null + }, + "status": { + "value": null + } + }, + "refresh": {}, + "audioNotification": {}, + "execute": { + "data": { + "value": null + } + }, + "samsungvd.audioInputSource": { + "supportedInputSources": { + "value": ["D.IN", "BT", "WIFI"], + "timestamp": "2025-03-18T19:11:54.071Z" + }, + "inputSource": { + "value": "D.IN", + "timestamp": "2025-04-17T11:18:02.048Z" + } + }, + "switch": { + "switch": { + "value": "off", + "timestamp": "2025-04-17T14:42:04.704Z" + } + }, + "sec.wifiConfiguration": { + "autoReconnection": { + "value": true, + "timestamp": "2025-03-18T19:11:54.484Z" + }, + "minVersion": { + "value": "1.0", + "timestamp": "2025-03-18T19:11:54.484Z" + }, + "supportedWiFiFreq": { + "value": ["2.4G", "5G"], + "timestamp": "2025-03-18T19:11:54.484Z" + }, + "supportedAuthType": { + "value": [ + "OPEN", + "WEP", + "WPA-PSK", + "WPA2-PSK", + "EAP", + "SAE", + "OWE", + "FT-PSK" + ], + "timestamp": "2025-03-18T19:11:54.484Z" + }, + "protocolType": { + "value": ["ble_ocf"], + "timestamp": "2025-03-18T19:11:54.484Z" + } + }, + "ocf": { + "st": { + "value": "1970-01-01T00:00:47Z", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "mndt": { + "value": "2024-01-01", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "mnfv": { + "value": "SAT-MT8532D24WWC-1016.0", + "timestamp": "2025-02-21T16:47:38.134Z" + }, + "mnhw": { + "value": "", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "di": { + "value": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "mnsl": { + "value": "", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "dmv": { + "value": "res.1.1.0,sh.1.1.0", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "n": { + "value": "Soundbar", + "timestamp": "2025-02-21T16:47:38.134Z" + }, + "mnmo": { + "value": "HW-S60D", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "vid": { + "value": "VD-NetworkAudio-003S", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "mnml": { + "value": "", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "mnpv": { + "value": "8.0", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "mnos": { + "value": "Tizen", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "pi": { + "value": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6", + "timestamp": "2025-02-21T15:09:52.348Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2025-02-21T15:09:52.348Z" + } + }, + "samsungvd.supportsFeatures": { + "mediaOutputSupported": { + "value": null + }, + "imeAdvSupported": { + "value": null + }, + "wifiUpdateSupport": { + "value": true, + "timestamp": "2025-03-18T19:11:53.853Z" + }, + "executableServiceList": { + "value": null + }, + "remotelessSupported": { + "value": null + }, + "artSupported": { + "value": null + }, + "mobileCamSupported": { + "value": null + } + }, + "sec.diagnosticsInformation": { + "logType": { + "value": ["errCode", "dump"], + "timestamp": "2025-03-18T19:11:54.336Z" + }, + "endpoint": { + "value": "PIPER", + "timestamp": "2025-03-18T19:11:54.336Z" + }, + "minVersion": { + "value": "3.0", + "timestamp": "2025-03-18T19:11:54.336Z" + }, + "signinPermission": { + "value": null + }, + "setupId": { + "value": "301", + "timestamp": "2025-03-18T19:11:54.336Z" + }, + "protocolType": { + "value": "ble_ocf", + "timestamp": "2025-03-18T19:11:54.336Z" + }, + "tsId": { + "value": "VD02", + "timestamp": "2025-03-18T19:11:54.336Z" + }, + "mnId": { + "value": "0AJK", + "timestamp": "2025-03-18T19:11:54.336Z" + }, + "dumpType": { + "value": "file", + "timestamp": "2025-03-18T19:11:54.336Z" + } + }, + "audioMute": { + "mute": { + "value": "muted", + "timestamp": "2025-04-17T11:36:04.814Z" + } + }, + "samsungvd.thingStatus": { + "updatedTime": { + "value": 1744900925, + "timestamp": "2025-04-17T14:42:04.770Z" + }, + "status": { + "value": "Idle", + "timestamp": "2025-03-18T19:11:54.101Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/vd_network_audio_003s.json b/tests/components/smartthings/fixtures/devices/vd_network_audio_003s.json new file mode 100644 index 00000000000..428b0e635d5 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/vd_network_audio_003s.json @@ -0,0 +1,115 @@ +{ + "items": [ + { + "deviceId": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6", + "name": "Soundbar", + "label": "Soundbar", + "manufacturerName": "Samsung Electronics", + "presentationId": "VD-NetworkAudio-003S", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "6bdf6730-8167-488b-8645-d0c5046ff763", + "ownerId": "15f0ae72-da51-14e2-65cf-ef59ae867e7f", + "roomId": "3b0fe9a8-51d6-49cf-b64a-8a719013c0a7", + "deviceTypeName": "Samsung OCF Network Audio Player", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "ocf", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "switch", + "version": 1 + }, + { + "id": "audioVolume", + "version": 1 + }, + { + "id": "audioMute", + "version": 1 + }, + { + "id": "samsungvd.audioInputSource", + "version": 1 + }, + { + "id": "audioNotification", + "version": 1 + }, + { + "id": "samsungvd.soundFrom", + "version": 1 + }, + { + "id": "sec.diagnosticsInformation", + "version": 1 + }, + { + "id": "samsungvd.thingStatus", + "version": 1 + }, + { + "id": "samsungvd.supportsFeatures", + "version": 1 + }, + { + "id": "sec.wifiConfiguration", + "version": 1 + }, + { + "id": "samsungvd.audioGroupInfo", + "version": 1, + "ephemeral": true + } + ], + "categories": [ + { + "name": "NetworkAudio", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2025-02-21T14:25:21.843Z", + "profile": { + "id": "25504ad5-8563-3b07-8770-e52ad29a9c5a" + }, + "ocf": { + "ocfDeviceType": "oic.d.networkaudio", + "name": "Soundbar", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0", + "manufacturerName": "Samsung Electronics", + "modelNumber": "HW-S60D", + "platformVersion": "8.0", + "platformOS": "Tizen", + "hwVersion": "", + "firmwareVersion": "SAT-MT8532D24WWC-1016.0", + "vendorId": "VD-NetworkAudio-003S", + "vendorResourceClientServerVersion": "4.0.26", + "lastSignupTime": "2025-03-18T19:11:51.176292902Z", + "transferCandidate": false, + "additionalAuthCodeRequired": false + }, + "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 db8c3a6ccc5..181df338aca 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -1652,6 +1652,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[vd_network_audio_003s] + 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', + 'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'HW-S60D', + 'model_id': None, + 'name': 'Soundbar', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'SAT-MT8532D24WWC-1016.0', + 'via_device_id': None, + }) +# --- # name: test_devices[vd_sensor_light_2023] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_media_player.ambr b/tests/components/smartthings/snapshots/test_media_player.ambr index 83f9d19b9fa..8eca654abe3 100644 --- a/tests/components/smartthings/snapshots/test_media_player.ambr +++ b/tests/components/smartthings/snapshots/test_media_player.ambr @@ -231,6 +231,56 @@ 'state': 'on', }) # --- +# name: test_all_entities[vd_network_audio_003s][media_player.soundbar-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'media_player', + 'entity_category': None, + 'entity_id': 'media_player.soundbar', + '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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[vd_network_audio_003s][media_player.soundbar-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speaker', + 'friendly_name': 'Soundbar', + 'supported_features': , + }), + 'context': , + 'entity_id': 'media_player.soundbar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[vd_stv_2017_k][media_player.tv_samsung_8_series_49-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 6264f9c67bdca7e6e8e1f778cb3c4dfd219cbfc6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 18 Apr 2025 19:41:18 +0200 Subject: [PATCH 245/258] Fix missing binary sensor for CoolSelect+ in SmartThings (#143216) --- .../components/smartthings/binary_sensor.py | 3 +- .../components/smartthings/strings.json | 3 + tests/components/smartthings/conftest.py | 1 + .../device_status/da_ref_normal_01001.json | 929 ++++++++++++++++++ .../fixtures/devices/da_ref_normal_01001.json | 433 ++++++++ .../snapshots/test_binary_sensor.ambr | 144 +++ .../smartthings/snapshots/test_button.ambr | 47 + .../smartthings/snapshots/test_init.ambr | 33 + .../smartthings/snapshots/test_sensor.ambr | 277 ++++++ .../smartthings/snapshots/test_switch.ambr | 47 + 10 files changed, 1916 insertions(+), 1 deletion(-) create mode 100644 tests/components/smartthings/fixtures/device_status/da_ref_normal_01001.json create mode 100644 tests/components/smartthings/fixtures/devices/da_ref_normal_01001.json diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 0fe0e7fe919..74d561f08ac 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -59,10 +59,11 @@ CAPABILITY_TO_SENSORS: dict[ Category.DOOR: BinarySensorDeviceClass.DOOR, Category.WINDOW: BinarySensorDeviceClass.WINDOW, }, - exists_fn=lambda key: key in {"freezer", "cooler"}, + exists_fn=lambda key: key in {"freezer", "cooler", "cvroom"}, component_translation_key={ "freezer": "freezer_door", "cooler": "cooler_door", + "cvroom": "cool_select_plus_door", }, deprecated_fn=( lambda status: "fridge_door" diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 5e18dada260..90792d21660 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -48,6 +48,9 @@ "cooler_door": { "name": "Cooler door" }, + "cool_select_plus_door": { + "name": "CoolSelect+ door" + }, "remote_control": { "name": "Remote control" }, diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 536c1275ef7..aa29a610620 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -107,6 +107,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "centralite", "da_ref_normal_000001", "da_ref_normal_01011", + "da_ref_normal_01001", "vd_network_audio_002s", "vd_network_audio_003s", "vd_sensor_light_2023", diff --git a/tests/components/smartthings/fixtures/device_status/da_ref_normal_01001.json b/tests/components/smartthings/fixtures/device_status/da_ref_normal_01001.json new file mode 100644 index 00000000000..aa73068f8bd --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_ref_normal_01001.json @@ -0,0 +1,929 @@ +{ + "components": { + "pantry-01": { + "samsungce.foodDefrost": { + "supportedOptions": { + "value": null + }, + "foodType": { + "value": null + }, + "weight": { + "value": null + }, + "operationTime": { + "value": null + }, + "remainingTime": { + "value": null + } + }, + "samsungce.fridgePantryInfo": { + "name": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.meatAging", "samsungce.foodDefrost"], + "timestamp": "2022-02-07T10:54:05.580Z" + } + }, + "samsungce.meatAging": { + "zoneInfo": { + "value": null + }, + "supportedMeatTypes": { + "value": null + }, + "supportedAgingMethods": { + "value": null + }, + "status": { + "value": null + } + }, + "samsungce.fridgePantryMode": { + "mode": { + "value": null + }, + "supportedModes": { + "value": null + } + } + }, + "icemaker": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [], + "timestamp": "2022-02-07T10:54:05.580Z" + } + }, + "switch": { + "switch": { + "value": "on", + "timestamp": "2025-02-07T12:01:52.528Z" + } + } + }, + "scale-10": { + "samsungce.connectionState": { + "connectionState": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [], + "timestamp": "2022-02-07T10:54:05.580Z" + } + }, + "samsungce.weightMeasurement": { + "weight": { + "value": null + } + }, + "samsungce.scaleSettings": { + "enabled": { + "value": null + } + }, + "samsungce.weightMeasurementCalibration": {} + }, + "scale-11": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [], + "timestamp": "2022-02-07T10:54:05.580Z" + } + }, + "samsungce.weightMeasurement": { + "weight": { + "value": null + } + } + }, + "camera-01": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["switch"], + "timestamp": "2023-12-17T11:19:18.845Z" + } + }, + "switch": { + "switch": { + "value": null + } + } + }, + "cooler": { + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2025-02-09T00:23:41.655Z" + } + }, + "samsungce.unavailableCapabilities": { + "unavailableCommands": { + "value": [], + "timestamp": "2024-11-06T12:35:50.411Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [], + "timestamp": "2024-06-17T06:16:33.918Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 37, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 34, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + }, + "maximumSetpoint": { + "value": 44, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": { + "minimum": 34, + "maximum": 44, + "step": 1 + }, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + }, + "coolingSetpoint": { + "value": 37, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + } + } + }, + "freezer": { + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2025-02-09T00:00:44.267Z" + } + }, + "samsungce.unavailableCapabilities": { + "unavailableCommands": { + "value": [], + "timestamp": "2024-11-06T12:35:50.411Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.freezerConvertMode"], + "timestamp": "2024-11-06T09:00:29.743Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 0, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": -8, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + }, + "maximumSetpoint": { + "value": 5, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + } + }, + "samsungce.freezerConvertMode": { + "supportedFreezerConvertModes": { + "value": [], + "timestamp": "2025-02-01T19:39:00.448Z" + }, + "freezerConvertMode": { + "value": null + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": { + "minimum": -8, + "maximum": 5, + "step": 1 + }, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + }, + "coolingSetpoint": { + "value": 0, + "unit": "F", + "timestamp": "2025-02-01T19:39:00.493Z" + } + } + }, + "main": { + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2025-02-09T00:23:41.655Z" + } + }, + "samsungce.viewInside": { + "supportedFocusAreas": { + "value": ["mainShelves"], + "timestamp": "2025-02-01T19:39:00.946Z" + }, + "contents": { + "value": [ + { + "fileId": "d3e1f875-f8b3-a031-737b-366eaa227773", + "mimeType": "image/jpeg", + "expiredTime": "2025-01-20T16:17:04Z", + "focusArea": "mainShelves" + }, + { + "fileId": "9fccb6b4-e71f-6c7f-9935-f6082bb6ccfe", + "mimeType": "image/jpeg", + "expiredTime": "2025-01-20T16:17:04Z", + "focusArea": "mainShelves" + }, + { + "fileId": "20b57a4d-b7fc-17fc-3a03-0fb84fb4efab", + "mimeType": "image/jpeg", + "expiredTime": "2025-01-20T16:17:05Z", + "focusArea": "mainShelves" + } + ], + "timestamp": "2025-01-20T16:07:05.423Z" + }, + "lastUpdatedTime": { + "value": "2025-02-07T12:01:52Z", + "timestamp": "2025-02-07T12:01:52.585Z" + } + }, + "samsungce.fridgeFoodList": { + "outOfSyncChanges": { + "value": null + }, + "refreshResult": { + "value": null + } + }, + "samsungce.deviceIdentification": { + "micomAssayCode": { + "value": null + }, + "modelName": { + "value": null + }, + "serialNumber": { + "value": null + }, + "serialNumberExtra": { + "value": null + }, + "modelClassificationCode": { + "value": null + }, + "description": { + "value": null + }, + "releaseYear": { + "value": 19, + "timestamp": "2024-11-06T09:00:29.743Z" + }, + "binaryId": { + "value": "24K_REF_LCD_FHUB9.0", + "timestamp": "2025-02-07T12:01:53.067Z" + } + }, + "samsungce.quickControl": { + "version": { + "value": "1.0", + "timestamp": "2025-02-01T19:39:01.848Z" + } + }, + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "ocf": { + "st": { + "value": "2024-11-08T11:56:59Z", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mndt": { + "value": "", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mnfv": { + "value": "20240616.213423", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mnhw": { + "value": "", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "di": { + "value": "7d3feb98-8a36-4351-c362-5e21ad3a78dd", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mnsl": { + "value": "", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "dmv": { + "value": "res.1.1.0,sh.1.1.0", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "n": { + "value": "Family Hub", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mnmo": { + "value": "24K_REF_LCD_FHUB9.0|00113141|0002034e051324200103000000000000", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "vid": { + "value": "DA-REF-NORMAL-01001", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mnml": { + "value": "", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mnpv": { + "value": "7.0", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "mnos": { + "value": "Tizen", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "pi": { + "value": "7d3feb98-8a36-4351-c362-5e21ad3a78dd", + "timestamp": "2025-01-02T12:37:43.756Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2025-01-02T12:37:43.756Z" + } + }, + "samsungce.fridgeVacationMode": { + "vacationMode": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [ + "thermostatCoolingSetpoint", + "temperatureMeasurement", + "custom.fridgeMode", + "custom.deviceReportStateConfiguration", + "samsungce.fridgeFoodList", + "samsungce.runestoneHomeContext", + "demandResponseLoadControl", + "samsungce.fridgeVacationMode", + "samsungce.sabbathMode" + ], + "timestamp": "2025-02-08T23:57:45.739Z" + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 24090102, + "timestamp": "2024-11-06T09:00:29.743Z" + } + }, + "sec.diagnosticsInformation": { + "logType": { + "value": ["errCode", "dump"], + "timestamp": "2025-02-01T19:39:00.523Z" + }, + "endpoint": { + "value": "SSM", + "timestamp": "2025-02-01T19:39:00.523Z" + }, + "minVersion": { + "value": "1.0", + "timestamp": "2025-02-01T19:39:00.523Z" + }, + "signinPermission": { + "value": null + }, + "setupId": { + "value": "500", + "timestamp": "2025-02-01T19:39:00.523Z" + }, + "protocolType": { + "value": "wifi_https", + "timestamp": "2025-02-01T19:39:00.523Z" + }, + "tsId": { + "value": null + }, + "mnId": { + "value": "0AJT", + "timestamp": "2025-02-01T19:39:00.523Z" + }, + "dumpType": { + "value": "file", + "timestamp": "2025-02-01T19:39:00.523Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": null + } + }, + "custom.deviceReportStateConfiguration": { + "reportStateRealtimePeriod": { + "value": null + }, + "reportStateRealtime": { + "value": { + "state": "disabled" + }, + "timestamp": "2025-02-01T19:39:00.345Z" + }, + "reportStatePeriod": { + "value": "enabled", + "timestamp": "2025-02-01T19:39:00.345Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": null + } + }, + "custom.disabledComponents": { + "disabledComponents": { + "value": [ + "icemaker-02", + "icemaker-03", + "pantry-01", + "camera-01", + "scale-10", + "scale-11" + ], + "timestamp": "2025-02-07T12:01:52.638Z" + } + }, + "demandResponseLoadControl": { + "drlcStatus": { + "value": { + "drlcType": 1, + "drlcLevel": 0, + "duration": 0, + "override": false + }, + "timestamp": "2025-02-01T19:38:59.899Z" + } + }, + "samsungce.sabbathMode": { + "supportedActions": { + "value": null + }, + "status": { + "value": null + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "energy": 4381422, + "deltaEnergy": 27, + "power": 144, + "powerEnergy": 27.01890500307083, + "persistedEnergy": 0, + "energySaved": 0, + "start": "2025-02-09T00:13:39Z", + "end": "2025-02-09T00:25:23Z" + }, + "timestamp": "2025-02-09T00:25:23.843Z" + } + }, + "refresh": {}, + "samsungce.runestoneHomeContext": { + "supportedContexts": { + "value": [ + { + "context": "HOME_IN", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "ASLEEP", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "AWAKE", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "COOKING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_COOKING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "EATING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_EATING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "DOING_LAUNDRY", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_DOING_LAUNDRY", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "CLEANING_HOUSE", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_CLEANING_HOUSE", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "MUSIC_LISTENING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_MUSIC_LISTENING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "AIR_CONDITIONING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_AIR_CONDITIONING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "WASHING_DISHES", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_WASHING_DISHES", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "CARING_CLOTHING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_CARING_CLOTHING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "WATCHING_TV", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_WATCHING_TV", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "BEFORE_BEDTIME", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "BEFORE_COOKING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "BEFORE_HOME_OUT", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "ORDERING_DELIVERY_FOOD", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_ORDERING_DELIVERY_FOOD", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "ONLINE_GROCERY_SHOPPING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + }, + { + "context": "FINISH_ONLINE_GROCERY_SHOPPING", + "place": "HOME", + "startTime": "99:99", + "endTime": "99:99" + } + ], + "timestamp": "2025-02-01T19:39:02.150Z" + } + }, + "execute": { + "data": { + "value": { + "payload": { + "rt": ["x.com.samsung.da.fridge"], + "if": ["oic.if.a"], + "x.com.samsung.da.rapidFridge": "Off", + "x.com.samsung.da.rapidFreezing": "Off" + } + }, + "data": { + "href": "/refrigeration/vs/0" + }, + "timestamp": "2024-03-26T09:06:17.169Z" + } + }, + "sec.wifiConfiguration": { + "autoReconnection": { + "value": true, + "timestamp": "2025-02-01T19:39:01.951Z" + }, + "minVersion": { + "value": "1.0", + "timestamp": "2025-02-01T19:39:01.951Z" + }, + "supportedWiFiFreq": { + "value": ["2.4G", "5G"], + "timestamp": "2025-02-01T19:39:01.951Z" + }, + "supportedAuthType": { + "value": ["OPEN", "WEP", "WPA-PSK", "WPA2-PSK"], + "timestamp": "2025-02-01T19:39:01.951Z" + }, + "protocolType": { + "value": ["helper_hotspot"], + "timestamp": "2025-02-01T19:39:01.951Z" + } + }, + "refrigeration": { + "defrost": { + "value": "off", + "timestamp": "2025-02-01T19:38:59.276Z" + }, + "rapidCooling": { + "value": "off", + "timestamp": "2025-02-01T19:39:00.497Z" + }, + "rapidFreezing": { + "value": "off", + "timestamp": "2025-02-01T19:39:00.497Z" + } + }, + "samsungce.powerCool": { + "activated": { + "value": false, + "timestamp": "2025-02-01T19:39:00.497Z" + } + }, + "custom.energyType": { + "energyType": { + "value": "2.0", + "timestamp": "2022-02-07T10:54:05.580Z" + }, + "energySavingSupport": { + "value": false, + "timestamp": "2022-02-07T10:57:35.490Z" + }, + "drMaxDuration": { + "value": 1440, + "unit": "min", + "timestamp": "2022-02-07T11:50:40.228Z" + }, + "energySavingLevel": { + "value": null + }, + "energySavingInfo": { + "value": null + }, + "supportedEnergySavingLevels": { + "value": null + }, + "energySavingOperation": { + "value": null + }, + "notificationTemplateID": { + "value": null + }, + "energySavingOperationSupport": { + "value": false, + "timestamp": "2022-02-07T11:50:40.228Z" + } + }, + "samsungce.softwareUpdate": { + "targetModule": { + "value": {}, + "timestamp": "2025-02-01T19:39:00.200Z" + }, + "otnDUID": { + "value": "2DCEZFTFQZPMO", + "timestamp": "2025-02-01T19:39:00.523Z" + }, + "lastUpdatedDate": { + "value": null + }, + "availableModules": { + "value": [], + "timestamp": "2025-02-01T19:39:00.523Z" + }, + "newVersionAvailable": { + "value": false, + "timestamp": "2025-02-01T19:39:00.200Z" + }, + "operatingState": { + "value": null + }, + "progress": { + "value": null + } + }, + "samsungce.powerFreeze": { + "activated": { + "value": false, + "timestamp": "2025-02-01T19:39:00.497Z" + } + }, + "custom.waterFilter": { + "waterFilterUsageStep": { + "value": 1, + "timestamp": "2025-02-01T19:38:59.973Z" + }, + "waterFilterResetType": { + "value": ["replaceable"], + "timestamp": "2025-02-01T19:38:59.973Z" + }, + "waterFilterCapacity": { + "value": null + }, + "waterFilterLastResetDate": { + "value": null + }, + "waterFilterUsage": { + "value": 52, + "timestamp": "2025-02-08T05:06:45.769Z" + }, + "waterFilterStatus": { + "value": "normal", + "timestamp": "2025-02-01T19:38:59.973Z" + } + } + }, + "cvroom": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": "CV_FDR_DELI", + "timestamp": "2025-02-01T19:39:00.448Z" + }, + "supportedFridgeModes": { + "value": [ + "CV_FDR_WINE", + "CV_FDR_DELI", + "CV_FDR_BEVERAGE", + "CV_FDR_MEAT" + ], + "timestamp": "2025-02-01T19:39:00.448Z" + } + }, + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2025-02-08T23:22:04.631Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [], + "timestamp": "2021-07-27T01:19:43.145Z" + } + } + }, + "icemaker-02": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [], + "timestamp": "2022-07-28T18:47:07.039Z" + } + }, + "switch": { + "switch": { + "value": null + } + } + }, + "icemaker-03": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [], + "timestamp": "2023-12-15T01:05:09.803Z" + } + }, + "switch": { + "switch": { + "value": null + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/da_ref_normal_01001.json b/tests/components/smartthings/fixtures/devices/da_ref_normal_01001.json new file mode 100644 index 00000000000..ade24657f26 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_ref_normal_01001.json @@ -0,0 +1,433 @@ +{ + "items": [ + { + "deviceId": "7d3feb98-8a36-4351-c362-5e21ad3a78dd", + "name": "Family Hub", + "label": "Refrigerator", + "manufacturerName": "Samsung Electronics", + "presentationId": "DA-REF-NORMAL-01001", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "2487472a-06c4-4bce-8f4c-700c5f8644f8", + "ownerId": "b603d7e8-6066-4e10-8102-afa752a63816", + "roomId": "acaa060a-7c19-4579-8a4a-5ad891a2f0c1", + "deviceTypeName": "Samsung OCF Refrigerator", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "ocf", + "version": 1 + }, + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "demandResponseLoadControl", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "refrigeration", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.deviceReportStateConfiguration", + "version": 1 + }, + { + "id": "custom.energyType", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.disabledComponents", + "version": 1 + }, + { + "id": "custom.waterFilter", + "version": 1 + }, + { + "id": "samsungce.fridgeFoodList", + "version": 1 + }, + { + "id": "samsungce.softwareUpdate", + "version": 1 + }, + { + "id": "samsungce.deviceIdentification", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.fridgeVacationMode", + "version": 1 + }, + { + "id": "samsungce.powerCool", + "version": 1 + }, + { + "id": "samsungce.powerFreeze", + "version": 1 + }, + { + "id": "samsungce.sabbathMode", + "version": 1 + }, + { + "id": "samsungce.viewInside", + "version": 1 + }, + { + "id": "samsungce.runestoneHomeContext", + "version": 1 + }, + { + "id": "samsungce.quickControl", + "version": 1 + }, + { + "id": "sec.diagnosticsInformation", + "version": 1 + }, + { + "id": "sec.wifiConfiguration", + "version": 1 + } + ], + "categories": [ + { + "name": "Refrigerator", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "freezer", + "label": "freezer", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.freezerConvertMode", + "version": 1 + }, + { + "id": "samsungce.unavailableCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "cooler", + "label": "cooler", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.unavailableCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "cvroom", + "label": "cvroom", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "icemaker", + "label": "icemaker", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "icemaker-02", + "label": "icemaker-02", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "icemaker-03", + "label": "icemaker-03", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "scale-10", + "label": "scale-10", + "capabilities": [ + { + "id": "samsungce.weightMeasurement", + "version": 1 + }, + { + "id": "samsungce.weightMeasurementCalibration", + "version": 1 + }, + { + "id": "samsungce.connectionState", + "version": 1 + }, + { + "id": "samsungce.scaleSettings", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "scale-11", + "label": "scale-11", + "capabilities": [ + { + "id": "samsungce.weightMeasurement", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "pantry-01", + "label": "pantry-01", + "capabilities": [ + { + "id": "samsungce.fridgePantryInfo", + "version": 1 + }, + { + "id": "samsungce.fridgePantryMode", + "version": 1 + }, + { + "id": "samsungce.meatAging", + "version": 1 + }, + { + "id": "samsungce.foodDefrost", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + }, + { + "id": "camera-01", + "label": "camera-01", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ] + } + ], + "createTime": "2021-07-27T01:19:42.051Z", + "profile": { + "id": "4c654f1b-8ef4-35b0-920e-c12568554213" + }, + "ocf": { + "ocfDeviceType": "oic.d.refrigerator", + "name": "Family Hub", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0", + "manufacturerName": "Samsung Electronics", + "modelNumber": "24K_REF_LCD_FHUB9.0|00113141|0002034e051324200103000000000000", + "platformVersion": "7.0", + "platformOS": "Tizen", + "hwVersion": "", + "firmwareVersion": "20240616.213423", + "vendorId": "DA-REF-NORMAL-01001", + "vendorResourceClientServerVersion": "4.0.22", + "locale": "", + "lastSignupTime": "2021-07-27T01:19:40.244392Z", + "transferCandidate": false, + "additionalAuthCodeRequired": false + }, + "type": "OCF", + "restrictionTier": 0, + "allowed": [], + "executionContext": "CLOUD" + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 2419a154e05..3aac14c819d 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -761,6 +761,150 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_01001][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': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_contactSensor_contact_contact', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01001][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_01001][binary_sensor.refrigerator_coolselect_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_coolselect_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': 'CoolSelect+ door', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cool_select_plus_door', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cvroom_contactSensor_contact_contact', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_coolselect_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Refrigerator CoolSelect+ door', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_coolselect_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_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.refrigerator_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': 'Freezer door', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'freezer_door', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_freezer_contactSensor_contact_contact', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_freezer_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Refrigerator Freezer door', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_freezer_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_cooler_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_button.ambr b/tests/components/smartthings/snapshots/test_button.ambr index 2c9dbd008af..4a7c582f608 100644 --- a/tests/components/smartthings/snapshots/test_button.ambr +++ b/tests/components/smartthings/snapshots/test_button.ambr @@ -187,3 +187,50 @@ 'state': 'unknown', }) # --- +# name: test_all_entities[da_ref_normal_01001][button.refrigerator_reset_water_filter-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.refrigerator_reset_water_filter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset water filter', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_water_filter', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_custom.waterFilter_resetWaterFilter', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01001][button.refrigerator_reset_water_filter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Reset water filter', + }), + 'context': , + 'entity_id': 'button.refrigerator_reset_water_filter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 181df338aca..59ad2cff19b 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -629,6 +629,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_ref_normal_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': '', + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '7d3feb98-8a36-4351-c362-5e21ad3a78dd', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': '24K_REF_LCD_FHUB9.0', + 'model_id': None, + 'name': 'Refrigerator', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '20240616.213423', + 'via_device_id': None, + }) +# --- # name: test_devices[da_ref_normal_01011] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index e9441f2e408..0abd65ef242 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -4049,6 +4049,283 @@ 'state': '0.0135559777781698', }) # --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_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.refrigerator_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': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_energy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Refrigerator Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4381.422', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_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.refrigerator_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': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Refrigerator Energy difference', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_energy_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.027', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_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.refrigerator_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': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_energySaved_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_saved-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Refrigerator Energy saved', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_energy_saved', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_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.refrigerator_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': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_power_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Refrigerator Power', + 'power_consumption_end': '2025-02-09T00:25:23Z', + 'power_consumption_start': '2025-02-09T00:13:39Z', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '144', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_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.refrigerator_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': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Refrigerator Power energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_power_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0270189050030708', + }) +# --- # name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index d14d4d02aa4..395a9943f98 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -93,6 +93,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_ice_maker-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_ice_maker', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ice maker', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ice_maker', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_icemaker_switch_switch_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_ice_maker-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Ice maker', + }), + 'context': , + 'entity_id': 'switch.refrigerator_ice_maker', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_all_entities[da_rvc_normal_000001][switch.robot_vacuum-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From b8793760a133e730eac962d30d962e74e6c32255 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 19 Apr 2025 09:17:04 +0000 Subject: [PATCH 246/258] Bump version to 2025.4.3 --- 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 ec6a6313542..6c29fbb42cd 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 = 4 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __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, 0) diff --git a/pyproject.toml b/pyproject.toml index 901ad944bec..d365ec9c8c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.2" +version = "2025.4.3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 7cbc3ea65f9d4269cfd87be95e2ea4860583f08e Mon Sep 17 00:00:00 2001 From: Arjan <44190435+vingerha@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:47:37 +0200 Subject: [PATCH 247/258] Meteofrance: adding new states provided by MF API since mid April (#143137) --- homeassistant/components/meteo_france/const.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 2230f43b754..e64a55651d3 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -74,6 +74,7 @@ CONDITION_CLASSES: dict[str, list[str]] = { "Pluie modérée", "Pluie / Averses", "Averses", + "Averses faibles", "Pluie", ], ATTR_CONDITION_SNOWY: [ @@ -81,10 +82,11 @@ CONDITION_CLASSES: dict[str, list[str]] = { "Neige", "Averses de neige", "Neige forte", + "Neige faible", "Quelques flocons", ], ATTR_CONDITION_SNOWY_RAINY: ["Pluie et neige", "Pluie verglaçante"], - ATTR_CONDITION_SUNNY: ["Ensoleillé"], + ATTR_CONDITION_SUNNY: ["Ensoleillé", "Ciel clair"], ATTR_CONDITION_WINDY: [], ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], From d8233b4de535c0a28a4559ab6c21a13b05df6742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Fri, 18 Apr 2025 00:09:52 +0200 Subject: [PATCH 248/258] Create Home Connect active and selected program entities only when there are programs (#143185) * Create active and selected program entities only when there are programs * Test improvements --- .../components/home_connect/coordinator.py | 4 +- .../components/home_connect/select.py | 3 +- .../home_connect/fixtures/programs.json | 24 +++++++++ .../snapshots/test_diagnostics.ambr | 3 ++ .../home_connect/test_binary_sensor.py | 35 +++++++++--- tests/components/home_connect/test_button.py | 46 +++++++++++----- tests/components/home_connect/test_light.py | 41 +++++++------- tests/components/home_connect/test_number.py | 31 ++++++++--- tests/components/home_connect/test_select.py | 53 +++++++++++++++++-- tests/components/home_connect/test_sensor.py | 29 +++++++--- tests/components/home_connect/test_switch.py | 45 +++++++++++----- tests/components/home_connect/test_time.py | 29 +++++++--- 12 files changed, 265 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/home_connect/coordinator.py b/homeassistant/components/home_connect/coordinator.py index 05a6a592ae8..0a0e0c78463 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -241,9 +241,7 @@ class HomeConnectCoordinator( appliance_data = await self._get_appliance_data( appliance_info, self.data.get(appliance_info.ha_id) ) - if event_message_ha_id in self.data: - self.data[event_message_ha_id].update(appliance_data) - else: + if event_message_ha_id not in self.data: self.data[event_message_ha_id] = appliance_data for listener, context in self._special_listeners.values(): if ( diff --git a/homeassistant/components/home_connect/select.py b/homeassistant/components/home_connect/select.py index c82e0686cb5..7d8b315b657 100644 --- a/homeassistant/components/home_connect/select.py +++ b/homeassistant/components/home_connect/select.py @@ -17,7 +17,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .common import setup_home_connect_entry from .const import ( - APPLIANCES_WITH_PROGRAMS, AVAILABLE_MAPS_ENUM, BEAN_AMOUNT_OPTIONS, BEAN_CONTAINER_OPTIONS, @@ -313,7 +312,7 @@ def _get_entities_for_appliance( HomeConnectProgramSelectEntity(entry.runtime_data, appliance, desc) for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS ] - if appliance.info.type in APPLIANCES_WITH_PROGRAMS + if appliance.programs else [] ), *[ diff --git a/tests/components/home_connect/fixtures/programs.json b/tests/components/home_connect/fixtures/programs.json index bba1a5d2721..e8d8bd24705 100644 --- a/tests/components/home_connect/fixtures/programs.json +++ b/tests/components/home_connect/fixtures/programs.json @@ -181,5 +181,29 @@ } ] } + }, + "Hood": { + "data": { + "programs": [ + { + "key": "Cooking.Common.Program.Hood.Automatic", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Cooking.Common.Program.Hood.Venting", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Cooking.Common.Program.Hood.DelayedShutOff", + "constraints": { + "execution": "selectandstart" + } + } + ] + } } } diff --git a/tests/components/home_connect/snapshots/test_diagnostics.ambr b/tests/components/home_connect/snapshots/test_diagnostics.ambr index 28f45ce97ba..535119b941c 100644 --- a/tests/components/home_connect/snapshots/test_diagnostics.ambr +++ b/tests/components/home_connect/snapshots/test_diagnostics.ambr @@ -90,6 +90,9 @@ 'ha_id': 'BOSCH-HCS000000-D00000000004', 'name': 'Hood', 'programs': list([ + 'Cooking.Common.Program.Hood.Automatic', + 'Cooking.Common.Program.Hood.Venting', + 'Cooking.Common.Program.Hood.DelayedShutOff', ]), 'settings': dict({ 'BSH.Common.Setting.AmbientLightBrightness': 70, diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index ce879a38de5..672d04e108b 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -11,6 +11,7 @@ from aiohomeconnect.model import ( EventMessage, EventType, HomeAppliance, + StatusKey, ) from aiohomeconnect.model.error import HomeConnectApiError import pytest @@ -115,9 +116,19 @@ async def test_paired_depaired_devices_flow( assert entity_registry.async_get(entity_entry.entity_id) -@pytest.mark.parametrize("appliance", ["Washer"], indirect=True) +@pytest.mark.parametrize( + ("appliance", "keys_to_check"), + [ + ( + "Washer", + (StatusKey.BSH_COMMON_REMOTE_CONTROL_ACTIVE,), + ) + ], + indirect=["appliance"], +) async def test_connected_devices( appliance: HomeAppliance, + keys_to_check: tuple, hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -148,7 +159,17 @@ async def test_connected_devices( device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) + assert entity_registry.async_get_entity_id( + Platform.BINARY_SENSOR, + DOMAIN, + f"{appliance.ha_id}-{EventKey.BSH_COMMON_APPLIANCE_CONNECTED}", + ) + for key in keys_to_check: + assert not entity_registry.async_get_entity_id( + Platform.BINARY_SENSOR, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) await client.add_events( [ @@ -161,10 +182,12 @@ async def test_connected_devices( ) await hass.async_block_till_done() - device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) - assert device - new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert len(new_entity_entries) > len(entity_entries) + for key in (*keys_to_check, EventKey.BSH_COMMON_APPLIANCE_CONNECTED): + assert entity_registry.async_get_entity_id( + Platform.BINARY_SENSOR, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/home_connect/test_button.py b/tests/components/home_connect/test_button.py index f894494792d..c96fe840238 100644 --- a/tests/components/home_connect/test_button.py +++ b/tests/components/home_connect/test_button.py @@ -99,9 +99,19 @@ async def test_paired_depaired_devices_flow( assert entity_registry.async_get(entity_entry.entity_id) -@pytest.mark.parametrize("appliance", ["Washer"], indirect=True) +@pytest.mark.parametrize( + ("appliance", "keys_to_check"), + [ + ( + "Washer", + (CommandKey.BSH_COMMON_PAUSE_PROGRAM,), + ) + ], + indirect=["appliance"], +) async def test_connected_devices( appliance: HomeAppliance, + keys_to_check: tuple, hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -116,7 +126,7 @@ async def test_connected_devices( not be obtained while disconnected and once connected, the entities are added. """ get_available_commands_original_mock = client.get_available_commands - get_available_programs_mock = client.get_available_programs + get_all_programs_mock = client.get_all_programs async def get_available_commands_side_effect(ha_id: str): if ha_id == appliance.ha_id: @@ -125,28 +135,36 @@ async def test_connected_devices( ) return await get_available_commands_original_mock.side_effect(ha_id) - async def get_available_programs_side_effect(ha_id: str): + 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_available_programs_mock.side_effect(ha_id) + return await get_all_programs_mock.side_effect(ha_id) client.get_available_commands = AsyncMock( side_effect=get_available_commands_side_effect ) - client.get_available_programs = AsyncMock( - side_effect=get_available_programs_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 - client.get_available_programs = get_available_programs_mock + client.get_all_programs = get_all_programs_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) + assert entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{appliance.ha_id}-StopProgram", + ) + for key in keys_to_check: + assert not entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) await client.add_events( [ @@ -159,10 +177,12 @@ async def test_connected_devices( ) await hass.async_block_till_done() - device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) - assert device - new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert len(new_entity_entries) > len(entity_entries) + for key in (*keys_to_check, "StopProgram"): + assert entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) diff --git a/tests/components/home_connect/test_light.py b/tests/components/home_connect/test_light.py index 50a1a1e374a..298eead1737 100644 --- a/tests/components/home_connect/test_light.py +++ b/tests/components/home_connect/test_light.py @@ -119,9 +119,19 @@ async def test_paired_depaired_devices_flow( assert entity_registry.async_get(entity_entry.entity_id) -@pytest.mark.parametrize("appliance", ["Hood"], indirect=True) +@pytest.mark.parametrize( + ("appliance", "keys_to_check"), + [ + ( + "Hood", + (SettingKey.COOKING_COMMON_LIGHTING,), + ) + ], + indirect=["appliance"], +) async def test_connected_devices( appliance: HomeAppliance, + keys_to_check: tuple, hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -136,7 +146,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_available_programs_mock = client.get_available_programs async def get_settings_side_effect(ha_id: str): if ha_id == appliance.ha_id: @@ -145,26 +154,20 @@ async def test_connected_devices( ) return await get_settings_original_mock.side_effect(ha_id) - async def get_available_programs_side_effect(ha_id: str): - if ha_id == appliance.ha_id: - raise HomeConnectApiError( - "SDK.Error.HomeAppliance.Connection.Initialization.Failed" - ) - return await get_available_programs_mock.side_effect(ha_id) - client.get_settings = AsyncMock(side_effect=get_settings_side_effect) - client.get_available_programs = AsyncMock( - side_effect=get_available_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 - client.get_available_programs = get_available_programs_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) + for key in keys_to_check: + assert not entity_registry.async_get_entity_id( + Platform.LIGHT, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) await client.add_events( [ @@ -177,10 +180,12 @@ async def test_connected_devices( ) await hass.async_block_till_done() - device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) - assert device - new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert len(new_entity_entries) > len(entity_entries) + for key in keys_to_check: + assert entity_registry.async_get_entity_id( + Platform.LIGHT, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) @pytest.mark.parametrize("appliance", ["Hood"], indirect=True) diff --git a/tests/components/home_connect/test_number.py b/tests/components/home_connect/test_number.py index 1de384303ce..7e89f66683b 100644 --- a/tests/components/home_connect/test_number.py +++ b/tests/components/home_connect/test_number.py @@ -135,9 +135,21 @@ async def test_paired_depaired_devices_flow( assert entity_registry.async_get(entity_entry.entity_id) -@pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True) +@pytest.mark.parametrize( + ("appliance", "keys_to_check"), + [ + ( + "FridgeFreezer", + ( + SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR, + ), + ) + ], + indirect=["appliance"], +) async def test_connected_devices( appliance: HomeAppliance, + keys_to_check: tuple, hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -168,7 +180,12 @@ async def test_connected_devices( device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) + for key in keys_to_check: + assert not entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) await client.add_events( [ @@ -181,10 +198,12 @@ async def test_connected_devices( ) await hass.async_block_till_done() - device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) - assert device - new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert len(new_entity_entries) > len(entity_entries) + for key in keys_to_check: + assert entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) @pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True) diff --git a/tests/components/home_connect/test_select.py b/tests/components/home_connect/test_select.py index f6009640f72..4f3f804eb06 100644 --- a/tests/components/home_connect/test_select.py +++ b/tests/components/home_connect/test_select.py @@ -20,6 +20,7 @@ from aiohomeconnect.model import ( ) from aiohomeconnect.model.error import ( ActiveProgramNotSetError, + HomeConnectApiError, HomeConnectError, SelectedProgramNotSetError, TooManyRequestsError, @@ -138,9 +139,23 @@ async def test_paired_depaired_devices_flow( assert entity_registry.async_get(entity_entry.entity_id) -@pytest.mark.parametrize("appliance", ["Washer"], indirect=True) +@pytest.mark.parametrize( + ("appliance", "keys_to_check"), + [ + ( + "Hood", + ( + EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM, + EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM, + SettingKey.COOKING_HOOD_COLOR_TEMPERATURE, + ), + ) + ], + indirect=["appliance"], +) async def test_connected_devices( appliance: HomeAppliance, + keys_to_check: tuple, hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -154,13 +169,39 @@ async def test_connected_devices( Specifically those devices whose settings, status, etc. could 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: + raise HomeConnectApiError( + "SDK.Error.HomeAppliance.Connection.Initialization.Failed" + ) + 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 config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == 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 + for key in keys_to_check: + assert not entity_registry.async_get_entity_id( + Platform.SELECT, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) await client.add_events( [ @@ -173,10 +214,12 @@ async def test_connected_devices( ) await hass.async_block_till_done() - device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) - assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert entity_entries + for key in keys_to_check: + assert entity_registry.async_get_entity_id( + Platform.SELECT, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index f30723af7fa..f0481318a37 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -154,9 +154,19 @@ async def test_paired_depaired_devices_flow( assert entity_registry.async_get(entity_entry.entity_id) -@pytest.mark.parametrize("appliance", ["Washer"], indirect=True) +@pytest.mark.parametrize( + ("appliance", "keys_to_check"), + [ + ( + "Washer", + (StatusKey.BSH_COMMON_OPERATION_STATE,), + ) + ], + indirect=["appliance"], +) async def test_connected_devices( appliance: HomeAppliance, + keys_to_check: tuple, hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -187,7 +197,12 @@ async def test_connected_devices( device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) + for key in keys_to_check: + assert not entity_registry.async_get_entity_id( + Platform.SENSOR, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) await client.add_events( [ @@ -200,10 +215,12 @@ async def test_connected_devices( ) await hass.async_block_till_done() - device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) - assert device - new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert len(new_entity_entries) > len(entity_entries) + for key in keys_to_check: + assert entity_registry.async_get_entity_id( + Platform.SENSOR, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) @pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index 01f9cad5d2e..2f8b95ceab2 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -147,9 +147,23 @@ async def test_paired_depaired_devices_flow( assert entity_registry.async_get(entity_entry.entity_id) -@pytest.mark.parametrize("appliance", ["Washer"], indirect=True) +@pytest.mark.parametrize( + ("appliance", "keys_to_check"), + [ + ( + "Washer", + ( + SettingKey.BSH_COMMON_POWER_STATE, + SettingKey.BSH_COMMON_CHILD_LOCK, + "Program Cotton", + ), + ) + ], + indirect=["appliance"], +) async def test_connected_devices( appliance: HomeAppliance, + keys_to_check: tuple, hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -164,7 +178,7 @@ 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_available_programs_mock = client.get_available_programs + get_all_programs_mock = client.get_all_programs async def get_settings_side_effect(ha_id: str): if ha_id == appliance.ha_id: @@ -173,26 +187,29 @@ async def test_connected_devices( ) return await get_settings_original_mock.side_effect(ha_id) - async def get_available_programs_side_effect(ha_id: str): + 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_available_programs_mock.side_effect(ha_id) + return await get_all_programs_mock.side_effect(ha_id) client.get_settings = AsyncMock(side_effect=get_settings_side_effect) - client.get_available_programs = AsyncMock( - side_effect=get_available_programs_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 - client.get_available_programs = get_available_programs_mock + client.get_all_programs = get_all_programs_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) + for key in keys_to_check: + assert not entity_registry.async_get_entity_id( + Platform.SWITCH, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) await client.add_events( [ @@ -205,10 +222,12 @@ async def test_connected_devices( ) await hass.async_block_till_done() - device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) - assert device - new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert len(new_entity_entries) > len(entity_entries) + for key in keys_to_check: + assert entity_registry.async_get_entity_id( + Platform.SWITCH, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/home_connect/test_time.py b/tests/components/home_connect/test_time.py index 8c23a09053a..34781c29eb8 100644 --- a/tests/components/home_connect/test_time.py +++ b/tests/components/home_connect/test_time.py @@ -113,9 +113,19 @@ async def test_paired_depaired_devices_flow( @pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.parametrize("appliance", ["Oven"], indirect=True) +@pytest.mark.parametrize( + ("appliance", "keys_to_check"), + [ + ( + "Oven", + (SettingKey.BSH_COMMON_ALARM_CLOCK,), + ) + ], + indirect=["appliance"], +) async def test_connected_devices( appliance: HomeAppliance, + keys_to_check: tuple, hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], @@ -146,7 +156,12 @@ async def test_connected_devices( device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) + for key in keys_to_check: + assert not entity_registry.async_get_entity_id( + Platform.TIME, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) await client.add_events( [ @@ -159,10 +174,12 @@ async def test_connected_devices( ) await hass.async_block_till_done() - device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) - assert device - new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert len(new_entity_entries) > len(entity_entries) + for key in keys_to_check: + assert entity_registry.async_get_entity_id( + Platform.TIME, + DOMAIN, + f"{appliance.ha_id}-{key}", + ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") From 2e05dc86182e4b995e385326f783c988f46bfa59 Mon Sep 17 00:00:00 2001 From: Arjan <44190435+vingerha@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:44:36 +0200 Subject: [PATCH 249/258] =?UTF-8?q?M=C3=A9t=C3=A9o-France:=20Additional=20?= =?UTF-8?q?states=20and=20change=20weather=20condition=20for=20"Ciel=20cla?= =?UTF-8?q?ir"=20(#143198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Additional new states and change for ciel-clair * Adding new previously unmapped state * Adding new forecast state Adding Brouillard dense, reported after the review --- homeassistant/components/meteo_france/const.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index e64a55651d3..382a56d50d7 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -40,7 +40,7 @@ ATTR_NEXT_RAIN_DT_REF = "forecast_time_ref" CONDITION_CLASSES: dict[str, list[str]] = { - ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire"], + ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire", "Ciel clair"], ATTR_CONDITION_CLOUDY: ["Très nuageux", "Couvert"], ATTR_CONDITION_FOG: [ "Brume ou bancs de brouillard", @@ -48,9 +48,10 @@ CONDITION_CLASSES: dict[str, list[str]] = { "Brouillard", "Brouillard givrant", "Bancs de Brouillard", + "Brouillard dense", ], ATTR_CONDITION_HAIL: ["Risque de grêle", "Risque de grèle"], - ATTR_CONDITION_LIGHTNING: ["Risque d'orages", "Orages"], + ATTR_CONDITION_LIGHTNING: ["Risque d'orages", "Orages", "Orage avec grêle"], ATTR_CONDITION_LIGHTNING_RAINY: [ "Pluie orageuses", "Pluies orageuses", @@ -62,6 +63,7 @@ CONDITION_CLASSES: dict[str, list[str]] = { "Éclaircies", "Eclaircies", "Peu nuageux", + "Variable", ], ATTR_CONDITION_POURING: ["Pluie forte"], ATTR_CONDITION_RAINY: [ @@ -83,10 +85,11 @@ CONDITION_CLASSES: dict[str, list[str]] = { "Averses de neige", "Neige forte", "Neige faible", + "Averses de neige faible", "Quelques flocons", ], ATTR_CONDITION_SNOWY_RAINY: ["Pluie et neige", "Pluie verglaçante"], - ATTR_CONDITION_SUNNY: ["Ensoleillé", "Ciel clair"], + ATTR_CONDITION_SUNNY: ["Ensoleillé"], ATTR_CONDITION_WINDY: [], ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], From 10ac39f6b2cbe6f988caae728817e2f011310a53 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 20 Apr 2025 03:57:53 +0200 Subject: [PATCH 250/258] Update setuptools to 78.1.1 (#143275) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d365ec9c8c5..b7866e2f234 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==77.0.3"] +requires = ["setuptools==78.1.1"] build-backend = "setuptools.build_meta" [project] From 73707fa23176c9d5bf4768597f49e376ec9c9ca0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 20 Apr 2025 02:24:44 +0200 Subject: [PATCH 251/258] Fix licenses check for setuptools (#143292) --- script/licenses.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/licenses.py b/script/licenses.py index ab8ab62eb1d..aed3bec9998 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -191,7 +191,6 @@ EXCEPTIONS = { "enocean", # https://github.com/kipe/enocean/pull/142 "imutils", # https://github.com/PyImageSearch/imutils/pull/292 "iso4217", # Public domain - "jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21 "kiwiki_client", # https://github.com/c7h/kiwiki_client/pull/6 "ld2410-ble", # https://github.com/930913/ld2410-ble/pull/7 "maxcube-api", # https://github.com/uebelack/python-maxcube-api/pull/48 @@ -205,6 +204,11 @@ 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 + "setuptools", # MIT } TODO = { From deb966128f71d0b98bf8dc838144da4b51bb6278 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:11:39 +0200 Subject: [PATCH 252/258] Add scan interval and parallel updates to LinkPlay media player (#143324) --- homeassistant/components/linkplay/media_player.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 16b0d5f75f1..67aa424e3a2 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -120,6 +121,8 @@ SERVICE_PLAY_PRESET_SCHEMA = cv.make_entity_service_schema( ) RETRY_POLL_MAXIMUM = 3 +SCAN_INTERVAL = timedelta(seconds=5) +PARALLEL_UPDATES = 1 async def async_setup_entry( From afa6ed09ef65a631efece53ed9b3c519bca55674 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:58:47 +0200 Subject: [PATCH 253/258] Sync random sensor device classes (#143368) --- 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 e5c5543e39f..bacd6dd5a17 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -84,8 +84,10 @@ "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%]", "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%]", From c38a3a239ccd0c0cb438df2aedfc88501b4bfb25 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 20 Apr 2025 23:56:09 +0200 Subject: [PATCH 254/258] Fix Vodafone Station config entry unload (#143371) --- .../components/vodafone_station/__init__.py | 2 -- tests/components/vodafone_station/conftest.py | 2 +- .../components/vodafone_station/test_init.py | 20 +++++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vodafone_station/__init__.py b/homeassistant/components/vodafone_station/__init__.py index 9f118fe4fbd..b4ba5663ac2 100644 --- a/homeassistant/components/vodafone_station/__init__.py +++ b/homeassistant/components/vodafone_station/__init__.py @@ -3,7 +3,6 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN from .coordinator import VodafoneConfigEntry, VodafoneStationRouter PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.SENSOR] @@ -36,7 +35,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: VodafoneConfigEntry) -> coordinator = entry.runtime_data await coordinator.api.logout() await coordinator.api.close() - hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/tests/components/vodafone_station/conftest.py b/tests/components/vodafone_station/conftest.py index a065a1e8065..778d8fdaa41 100644 --- a/tests/components/vodafone_station/conftest.py +++ b/tests/components/vodafone_station/conftest.py @@ -5,7 +5,7 @@ from datetime import UTC, datetime from aiovodafone import VodafoneStationDevice import pytest -from homeassistant.components.vodafone_station import DOMAIN +from homeassistant.components.vodafone_station.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from .const import DEVICE_1_HOST, DEVICE_1_MAC, DEVICE_2_MAC diff --git a/tests/components/vodafone_station/test_init.py b/tests/components/vodafone_station/test_init.py index 12b3c3dce8f..053f0a95fe4 100644 --- a/tests/components/vodafone_station/test_init.py +++ b/tests/components/vodafone_station/test_init.py @@ -3,6 +3,8 @@ from unittest.mock import AsyncMock from homeassistant.components.device_tracker import CONF_CONSIDER_HOME +from homeassistant.components.vodafone_station.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -31,3 +33,21 @@ async def test_reload_config_entry_with_options( assert result["data"] == { CONF_CONSIDER_HOME: 37, } + + +async def test_unload_entry( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unloading the config entry.""" + await setup_integration(hass, mock_config_entry) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert mock_config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) From b3deeca939576d026ff2959ef007181ea0d45122 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Apr 2025 23:00:40 -1000 Subject: [PATCH 255/258] Bump aiohomekit to 3.2.14 (#143440) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 6562a3edcc9..dbcd2788c8a 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.13"], + "requirements": ["aiohomekit==3.2.14"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 81284e09591..fbffea57bae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aiohasupervisor==0.3.0 aiohomeconnect==0.16.3 # homeassistant.components.homekit_controller -aiohomekit==3.2.13 +aiohomekit==3.2.14 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2fc32e6503..a0297b02563 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -252,7 +252,7 @@ aiohasupervisor==0.3.0 aiohomeconnect==0.16.3 # homeassistant.components.homekit_controller -aiohomekit==3.2.13 +aiohomekit==3.2.14 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 From 7392d5a30a547577f06ec2e4ee230edc8745b71e Mon Sep 17 00:00:00 2001 From: cnico Date: Wed, 23 Apr 2025 09:31:43 +0200 Subject: [PATCH 256/258] Bump dio-chacon-api to v1.2.2 (#143489) Bump dio-chacon-api to v1.2.2 to solve https://github.com/home-assistant/core/issues/142808 --- homeassistant/components/chacon_dio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/chacon_dio/manifest.json b/homeassistant/components/chacon_dio/manifest.json index edee24444f7..117982a7ab8 100644 --- a/homeassistant/components/chacon_dio/manifest.json +++ b/homeassistant/components/chacon_dio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/chacon_dio", "iot_class": "cloud_push", "loggers": ["dio_chacon_api"], - "requirements": ["dio-chacon-wifi-api==1.2.1"] + "requirements": ["dio-chacon-wifi-api==1.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index fbffea57bae..7e9d81a4149 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -784,7 +784,7 @@ devolo-home-control-api==0.18.3 devolo-plc-api==1.5.1 # homeassistant.components.chacon_dio -dio-chacon-wifi-api==1.2.1 +dio-chacon-wifi-api==1.2.2 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0297b02563..6e2a740373f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -675,7 +675,7 @@ devolo-home-control-api==0.18.3 devolo-plc-api==1.5.1 # homeassistant.components.chacon_dio -dio-chacon-wifi-api==1.2.1 +dio-chacon-wifi-api==1.2.2 # homeassistant.components.directv directv==0.4.0 From 6a2d733d853653ca33d5f9d4198aefc364ebaeec Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Apr 2025 18:13:58 +0200 Subject: [PATCH 257/258] Bump pysmartthings to 3.0.5 (#143586) --- 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 4cd27e49664..c682b5402c4 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.0.4"] + "requirements": ["pysmartthings==3.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7e9d81a4149..9c8bf7ae9aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2319,7 +2319,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.0.4 +pysmartthings==3.0.5 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e2a740373f..2abcb85e499 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1889,7 +1889,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.0.4 +pysmartthings==3.0.5 # homeassistant.components.smarty pysmarty2==0.10.2 From 2214d9b330cc8c148040f40978ab7913c0f4bee4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Apr 2025 06:54:02 +0000 Subject: [PATCH 258/258] Bump version to 2025.4.4 --- 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 6c29fbb42cd..25c5eaf019b 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 = 4 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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, 0) diff --git a/pyproject.toml b/pyproject.toml index b7866e2f234..c1a6ffbadff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.4.3" +version = "2025.4.4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3."