From 5b547843788ecf07b1feae833767ad61e814411e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Jul 2025 16:56:55 +0200 Subject: [PATCH 001/247] Bump version to 2025.8.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 2daa6d91db284..97e463f851ead 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 35a2bf2c7fb09..2fee88acceeb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0.dev0" +version = "2025.8.0b0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 0799ee9fbad534d9bcd5176d6d0228367ea5e69d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 30 Jul 2025 18:53:21 +0200 Subject: [PATCH 002/247] Fix translation string reference for MQTT climate subentry option (#149673) --- 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 22fb85780b054..c14bda008d1c3 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -426,7 +426,7 @@ }, "data_description": { "payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]", - "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]", + "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_on%]", "power_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".", "power_command_topic": "The MQTT topic to publish commands to change the climate power state. Sends the payload configured with payload \"on\" or payload \"off\". [Learn more.]({url}#power_command_topic)" } @@ -812,7 +812,7 @@ "min_humidity": "The minimum target humidity that can be set.", "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the humidity command topic.", "target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)", - "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the humidity state topic with.", + "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.", "target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)" } }, From d8c93d54d58a66ac9d0c23932861859183375239 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 30 Jul 2025 12:57:59 -0500 Subject: [PATCH 003/247] Bump intents to 2025.7.30 (#149678) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- tests/components/assist_pipeline/test_pipeline.py | 4 ++-- tests/components/conversation/snapshots/test_http.ambr | 8 ++++---- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ad0a4c9610284..31adffad0645e 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.6.23"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 819bb2f5c9a56..704fb282784e6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.110.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250730.0 -home-assistant-intents==2025.6.23 +home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/requirements_all.txt b/requirements_all.txt index f731ecc0e0d5d..c01d9eef3476f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1177,7 +1177,7 @@ holidays==0.77 home-assistant-frontend==20250730.0 # homeassistant.components.conversation -home-assistant-intents==2025.6.23 +home-assistant-intents==2025.7.30 # homeassistant.components.homematicip_cloud homematicip==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64931e1ef4e90..eada71b4f0278 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1026,7 +1026,7 @@ holidays==0.77 home-assistant-frontend==20250730.0 # homeassistant.components.conversation -home-assistant-intents==2025.6.23 +home-assistant-intents==2025.7.30 # homeassistant.components.homematicip_cloud homematicip==2.2.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 5168388c934e0..5776f6dfe1278 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -32,7 +32,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ hassil==2.2.3 \ - home-assistant-intents==2025.6.23 \ + home-assistant-intents==2025.7.30 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ pyspeex-noise==1.0.2 diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 5bc7b86c38c0b..0cb67302700b9 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -375,7 +375,7 @@ async def test_get_pipelines(hass: HomeAssistant) -> None: ("en", "us", "en", "en"), ("en", "uk", "en", "en"), ("pt", "pt", "pt", "pt"), - ("pt", "br", "pt-br", "pt"), + ("pt", "br", "pt-BR", "pt"), ], ) async def test_default_pipeline_no_stt_tts( @@ -428,7 +428,7 @@ async def test_default_pipeline_no_stt_tts( ("en", "us", "en", "en", "en", "en"), ("en", "uk", "en", "en", "en", "en"), ("pt", "pt", "pt", "pt", "pt", "pt"), - ("pt", "br", "pt-br", "pt", "pt-br", "pt-br"), + ("pt", "br", "pt-BR", "pt", "pt-br", "pt-br"), ], ) @pytest.mark.usefixtures("init_supporting_components") diff --git a/tests/components/conversation/snapshots/test_http.ambr b/tests/components/conversation/snapshots/test_http.ambr index 391fb609d657a..8f68274d37f0f 100644 --- a/tests/components/conversation/snapshots/test_http.ambr +++ b/tests/components/conversation/snapshots/test_http.ambr @@ -45,7 +45,7 @@ 'nl', 'pl', 'pt', - 'pt-br', + 'pt-BR', 'ro', 'ru', 'sk', @@ -60,9 +60,9 @@ 'uk', 'ur', 'vi', - 'zh-cn', - 'zh-hk', - 'zh-tw', + 'zh-CN', + 'zh-HK', + 'zh-TW', ]), }), dict({ From 3da3cf7f523e9c7f0778ef2b667c09bec32fba13 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:59:41 -0400 Subject: [PATCH 004/247] Bump ZHA to 0.0.64 (#149683) Co-authored-by: TheJulianJES Co-authored-by: abmantis --- homeassistant/components/zha/helpers.py | 22 +++++++++++++++- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/strings.json | 30 ++++++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_update.py | 25 +++++++++++++++++- 6 files changed, 78 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 084e1c882acf1..f5b44eb8fc4a0 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -74,7 +74,12 @@ from zha.exceptions import ZHAException from zha.mixins import LogMixin from zha.zigbee.cluster_handlers import ClusterBindEvent, ClusterConfigureReportingEvent -from zha.zigbee.device import ClusterHandlerConfigurationComplete, Device, ZHAEvent +from zha.zigbee.device import ( + ClusterHandlerConfigurationComplete, + Device, + DeviceFirmwareInfoUpdatedEvent, + ZHAEvent, +) from zha.zigbee.group import Group, GroupInfo, GroupMember from zigpy.config import ( CONF_DATABASE, @@ -843,8 +848,23 @@ def _async_get_or_create_device_proxy(self, zha_device: Device) -> ZHADeviceProx name=zha_device.name, manufacturer=zha_device.manufacturer, model=zha_device.model, + sw_version=zha_device.firmware_version, ) zha_device_proxy.device_id = device_registry_device.id + + def update_sw_version(event: DeviceFirmwareInfoUpdatedEvent) -> None: + """Update software version in device registry.""" + device_registry.async_update_device( + device_registry_device.id, + sw_version=event.new_firmware_version, + ) + + self._unsubs.append( + zha_device.on_event( + DeviceFirmwareInfoUpdatedEvent.event_type, update_sw_version + ) + ) + return zha_device_proxy def _async_get_or_create_group_proxy(self, group_info: GroupInfo) -> ZHAGroupProxy: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 2cbc962a305f9..ec08c4f5d9ddd 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.62"], + "requirements": ["zha==0.0.64"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 23d17ea128f16..1c9454ec0a068 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -616,6 +616,18 @@ }, "water_supply": { "name": "Water supply" + }, + "frient_in_1": { + "name": "IN1" + }, + "frient_in_2": { + "name": "IN2" + }, + "frient_in_3": { + "name": "IN3" + }, + "frient_in_4": { + "name": "IN4" } }, "button": { @@ -639,6 +651,9 @@ }, "frost_lock_reset": { "name": "Frost lock reset" + }, + "reset_alarm": { + "name": "Reset alarm" } }, "climate": { @@ -1472,6 +1487,9 @@ "tier6_summation_delivered": { "name": "Tier 6 summation delivered" }, + "total_active_power": { + "name": "Total power" + }, "summation_received": { "name": "Summation received" }, @@ -2006,6 +2024,18 @@ }, "auto_relock": { "name": "Autorelock" + }, + "distance_tracking": { + "name": "Distance tracking" + }, + "water_shortage_auto_close": { + "name": "Water shortage auto-close" + }, + "frient_com_1": { + "name": "COM 1" + }, + "frient_com_2": { + "name": "COM 2" } } } diff --git a/requirements_all.txt b/requirements_all.txt index c01d9eef3476f..f5f0c5116dc16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3203,7 +3203,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.62 +zha==0.0.64 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eada71b4f0278..9336bbcc68cdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2647,7 +2647,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.62 +zha==0.0.64 # homeassistant.components.zwave_js zwave-js-server-python==0.67.0 diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index c8cbc40710628..04d190b170c39 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -47,6 +47,7 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .common import find_entity_id, update_attribute_cache @@ -156,7 +157,6 @@ async def setup_test_data( ) ) zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) - zha_device_proxy.device.async_update_sw_build_id(installed_fw_version) return zha_device_proxy, cluster, fw_image, installed_fw_version @@ -643,3 +643,26 @@ async def test_update_release_notes( assert "Some lengthy release notes" in result["result"] assert OTA_MESSAGE_RELIABILITY in result["result"] assert OTA_MESSAGE_BATTERY_POWERED in result["result"] + + +async def test_update_version_sync_device_registry( + hass: HomeAssistant, + setup_zha, + zigpy_device_mock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test firmware version syncing between the ZHA device and Home Assistant.""" + await setup_zha() + zha_device, _, _, _ = await setup_test_data(hass, zigpy_device_mock) + + zha_device.device.async_update_firmware_version("0x12345678") + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.device.ieee))} + ) + assert reg_device.sw_version == "0x12345678" + + zha_device.device.async_update_firmware_version("0xabcd1234") + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.device.ieee))} + ) + assert reg_device.sw_version == "0xabcd1234" From 29daf136d2d8084edd076e83f83fb1291c9bfaf4 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:59:01 +0200 Subject: [PATCH 005/247] Fix `KeyError` in friends coordinator (#149684) --- .../components/playstation_network/coordinator.py | 8 +++++--- tests/components/playstation_network/conftest.py | 8 ++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/playstation_network/coordinator.py b/homeassistant/components/playstation_network/coordinator.py index c447e8dc5039c..977632de23bc2 100644 --- a/homeassistant/components/playstation_network/coordinator.py +++ b/homeassistant/components/playstation_network/coordinator.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from psnawp_api.core.psnawp_exceptions import ( PSNAWPAuthenticationError, @@ -29,7 +29,7 @@ ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_ACCOUNT_ID, DOMAIN +from .const import DOMAIN from .helpers import PlaystationNetwork, PlaystationNetworkData _LOGGER = logging.getLogger(__name__) @@ -176,7 +176,9 @@ def __init__( def _setup(self) -> None: """Set up the coordinator.""" - self.user = self.psn.psn.user(account_id=self.subentry.data[CONF_ACCOUNT_ID]) + if TYPE_CHECKING: + assert self.subentry.unique_id + self.user = self.psn.psn.user(account_id=self.subentry.unique_id) self.profile = self.user.profile() async def _async_setup(self) -> None: diff --git a/tests/components/playstation_network/conftest.py b/tests/components/playstation_network/conftest.py index ab4edc0e3f41e..bfbdc9a72bdd4 100644 --- a/tests/components/playstation_network/conftest.py +++ b/tests/components/playstation_network/conftest.py @@ -14,11 +14,7 @@ ) import pytest -from homeassistant.components.playstation_network.const import ( - CONF_ACCOUNT_ID, - CONF_NPSSO, - DOMAIN, -) +from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN from homeassistant.config_entries import ConfigSubentryData from tests.common import MockConfigEntry @@ -40,7 +36,7 @@ def mock_config_entry() -> MockConfigEntry: unique_id=PSN_ID, subentries_data=[ ConfigSubentryData( - data={CONF_ACCOUNT_ID: "fren-psn-id"}, + data={}, subentry_id="ABCDEF", subentry_type="friend", title="PublicUniversalFriend", From aa2941592d0acbe0f0aca56550adb3250f534367 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:06:04 +0200 Subject: [PATCH 006/247] Fix ContextVar deprecation warning in homeassistant_hardware integration (#149687) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com> Co-authored-by: mib1185 <35783820+mib1185@users.noreply.github.com> --- .../components/homeassistant_hardware/coordinator.py | 10 +++++++++- .../components/homeassistant_sky_connect/update.py | 1 + .../components/homeassistant_yellow/update.py | 1 + .../homeassistant_hardware/test_coordinator.py | 6 +++++- tests/components/homeassistant_hardware/test_update.py | 2 ++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/coordinator.py b/homeassistant/components/homeassistant_hardware/coordinator.py index c9a5c8913281c..36a2f407282b8 100644 --- a/homeassistant/components/homeassistant_hardware/coordinator.py +++ b/homeassistant/components/homeassistant_hardware/coordinator.py @@ -12,6 +12,7 @@ ManifestMissing, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,13 +25,20 @@ class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]): """Coordinator to manage firmware updates.""" - def __init__(self, hass: HomeAssistant, session: ClientSession, url: str) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + session: ClientSession, + url: str, + ) -> None: """Initialize the firmware update coordinator.""" super().__init__( hass, _LOGGER, name="firmware update coordinator", update_interval=FIRMWARE_REFRESH_INTERVAL, + config_entry=config_entry, ) self.hass = hass self.session = session diff --git a/homeassistant/components/homeassistant_sky_connect/update.py b/homeassistant/components/homeassistant_sky_connect/update.py index 74c28b37eaf6b..df69b6d40a23f 100644 --- a/homeassistant/components/homeassistant_sky_connect/update.py +++ b/homeassistant/components/homeassistant_sky_connect/update.py @@ -124,6 +124,7 @@ def _async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, NABU_CASA_FIRMWARE_RELEASES_URL, ), diff --git a/homeassistant/components/homeassistant_yellow/update.py b/homeassistant/components/homeassistant_yellow/update.py index 9531bd456cb93..7a6e2f19b1f0c 100644 --- a/homeassistant/components/homeassistant_yellow/update.py +++ b/homeassistant/components/homeassistant_yellow/update.py @@ -129,6 +129,7 @@ def _async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, NABU_CASA_FIRMWARE_RELEASES_URL, ), diff --git a/tests/components/homeassistant_hardware/test_coordinator.py b/tests/components/homeassistant_hardware/test_coordinator.py index 9c57aac681110..39fef3366adb1 100644 --- a/tests/components/homeassistant_hardware/test_coordinator.py +++ b/tests/components/homeassistant_hardware/test_coordinator.py @@ -13,6 +13,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry + async def test_firmware_update_coordinator_fetching( hass: HomeAssistant, caplog: pytest.LogCaptureFixture @@ -20,6 +22,8 @@ async def test_firmware_update_coordinator_fetching( """Test the firmware update coordinator loads manifests.""" session = async_get_clientsession(hass) + mock_config_entry = MockConfigEntry() + manifest = FirmwareManifest( url=URL("https://example.org/firmware"), html_url=URL("https://example.org/release_notes"), @@ -35,7 +39,7 @@ async def test_firmware_update_coordinator_fetching( return_value=mock_client, ): coordinator = FirmwareUpdateCoordinator( - hass, session, "https://example.org/firmware" + hass, mock_config_entry, session, "https://example.org/firmware" ) listener = Mock() diff --git a/tests/components/homeassistant_hardware/test_update.py b/tests/components/homeassistant_hardware/test_update.py index aacc064e4f24a..3103e5cfc6aa8 100644 --- a/tests/components/homeassistant_hardware/test_update.py +++ b/tests/components/homeassistant_hardware/test_update.py @@ -143,6 +143,7 @@ def _mock_async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, TEST_FIRMWARE_RELEASES_URL, ), @@ -593,6 +594,7 @@ async def test_update_entity_graceful_firmware_type_callback_errors( config_entry=update_config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + update_config_entry, session, TEST_FIRMWARE_RELEASES_URL, ), From 7eb7c66e3f3e8d83da02ae1f12d36f8b1fc72500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 30 Jul 2025 20:19:01 +0200 Subject: [PATCH 007/247] Explicitly pass config_entry to miele coordinator (#149691) --- homeassistant/components/miele/__init__.py | 2 +- homeassistant/components/miele/coordinator.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/miele/__init__.py b/homeassistant/components/miele/__init__.py index 1cb2fc0fab15a..2c5c250aee731 100644 --- a/homeassistant/components/miele/__init__.py +++ b/homeassistant/components/miele/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo ) from err # Setup MieleAPI and coordinator for data fetch - coordinator = MieleDataUpdateCoordinator(hass, auth) + coordinator = MieleDataUpdateCoordinator(hass, entry, auth) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index 27456ffe04c19..d5de2d79cb9c4 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -42,12 +42,14 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]): def __init__( self, hass: HomeAssistant, + config_entry: MieleConfigEntry, api: AsyncConfigEntryAuth, ) -> None: """Initialize the Miele data coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=120), ) From 59eace67df778e4fa408c5c3b90e44522bc2199d Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:30:05 -0400 Subject: [PATCH 008/247] Add translations for all fields in template integration (#149692) Co-authored-by: Norbert Rittel --- .../components/template/strings.json | 238 +++++++++++++++--- 1 file changed, 209 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index edf4516e8ab97..b412fa519cde6 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -2,6 +2,7 @@ "common": { "advanced_options": "Advanced options", "availability": "Availability template", + "availability_description": "Defines a template to get the `available` state of the entity. If the template either fails to render or returns `True`, `\"1\"`, `\"true\"`, `\"yes\"`, `\"on\"`, `\"enable\"`, or a non-zero number, the entity will be `available`. If the template returns any other value, the entity will be `unavailable`. If not configured, the entity will always be `available`. Note that the string comparison is not case sensitive; `\"TrUe\"` and `\"yEs\"` are allowed.", "code_format": "Code format", "device_class": "Device class", "device_id_description": "Select a device to link to this entity.", @@ -28,13 +29,26 @@ "code_format": "[%key:component::template::common::code_format%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "value_template": "Defines a template to set the state of the alarm panel. Valid output values from the template are `armed_away`, `armed_home`, `armed_night`, `armed_vacation`, `arming`, `disarmed`, `pending`, and `triggered`.", + "disarm": "Defines actions to run when the alarm control panel is disarmed. Receives variable `code`.", + "arm_away": "Defines actions to run when the alarm control panel is armed to `arm_away`. Receives variable `code`.", + "arm_custom_bypass": "Defines actions to run when the alarm control panel is armed to `arm_custom_bypass`. Receives variable `code`.", + "arm_home": "Defines actions to run when the alarm control panel is armed to `arm_home`. Receives variable `code`.", + "arm_night": "Defines actions to run when the alarm control panel is armed to `arm_night`. Receives variable `code`.", + "arm_vacation": "Defines actions to run when the alarm control panel is armed to `arm_vacation`. Receives variable `code`.", + "trigger": "Defines actions to run when the alarm control panel is triggered. Receives variable `code`.", + "code_arm_required": "If true, the code is required to arm the alarm.", + "code_format": "One of number, text or no_code. Format for the code used to arm/disarm the alarm." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -48,13 +62,17 @@ "state": "[%key:component::template::common::state%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "The sensor is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -68,13 +86,17 @@ "press": "Actions on press" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "press": "Defines actions to run when button is pressed." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -99,13 +121,16 @@ "close_cover": "Defines actions to run when the cover is closed.", "stop_cover": "Defines actions to run when the cover is stopped.", "position": "Defines a template to get the position of the cover. Value values are numbers between `0` (`closed`) and `100` (`open`).", - "set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command." + "set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command. Receives variable `position`." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -124,11 +149,11 @@ }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", - "state": "Defines a template to get the state of the fan. Valid values: `on`, `off`.", + "state": "The fan is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.", "turn_off": "Defines actions to run when the fan is turned off.", - "turn_on": "Defines actions to run when the fan is turned on.", + "turn_on": "Defines actions to run when the fan is turned on. Receives variables `percentage` and/or `preset_mode`.", "percentage": "Defines a template to get the speed percentage of the fan.", - "set_percentage": "Defines actions to run when the fan is given a speed percentage command.", + "set_percentage": "Defines actions to run when the fan is given a speed percentage command. Receives variable `percentage`.", "speed_count": "The number of speeds the fan supports. Used to calculate the percentage step for the `fan.increase_speed` and `fan.decrease_speed` actions." }, "sections": { @@ -136,6 +161,9 @@ "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -149,13 +177,18 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "url": "Defines a template to get the URL on which the image is served.", + "verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http-only URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -176,13 +209,25 @@ "set_temperature": "Actions on set color temperature" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "The light is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.", + "turn_off": "Defines actions to run when the light is turned off.", + "turn_on": "Defines actions to run when the light is turned on.", + "level": "Defines a template to get the brightness of the light. Valid values are 0 to 255.", + "set_level": "Defines actions to run when the light is given a brightness command. The script will only be called if the `turn_on` call only has `brightness`, and optionally `transition`. Receives variables `brightness` and, optionally, `transition`.", + "hs": "Defines a template to get the HS color of the light. Must render a tuple (hue, saturation).", + "set_hs": "Defines actions to run when the light is given a hs color command. Available variables: `hs` as a tuple, `h` and `s`.", + "temperature": "Defines a template to get the color temperature of the light.", + "set_temperature": "Defines actions to run when the light is given a color temperature command. Receives variable `color_temp_kelvin`. May also receive variables `brightness` and/or `transition`." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -199,13 +244,21 @@ "open": "Actions on open" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to set the state of the lock. The lock is locked if the template evaluates to `True`, `true`, `on`, or `locked`. The lock is unlocked if the template evaluates to `False`, `false`, `off`, or `unlocked`. Other valid states are `jammed`, `opening`, `locking`, `open`, and `unlocking`.", + "lock": "Defines actions to run when the lock is locked.", + "unlock": "Defines actions to run when the lock is unlocked.", + "code_format": "Defines a template to get the `code_format` attribute of the lock.", + "open": "Defines actions to run when the lock is opened." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -223,13 +276,22 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Template for the number's current value.", + "step": "Template for the number's increment/decrement step.", + "set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.", + "max": "Template for the number's maximum value.", + "min": "Template for the number's minimum value.", + "unit_of_measurement": "Defines the units of measurement of the number, if any." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -244,13 +306,19 @@ "options": "Available options" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Template for the select’s current value.", + "select_option": "Defines actions to run when an `option` from the `options` list is selected. Receives variable `option`.", + "options": "Template for the select’s available options." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -266,13 +334,18 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the sensor. If the sensor is numeric, i.e. it has a `state_class` or a `unit_of_measurement`, the state template must render to a number or to `none`. The state template must not render to a string, including `unknown` or `unavailable`. An `availability` template may be defined to suppress rendering of the state template.", + "unit_of_measurement": "Defines the units of measurement of the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -307,13 +380,18 @@ }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", - "value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful." + "value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful.", + "turn_off": "Defines actions to run when the switch is turned off.", + "turn_on": "Defines actions to run when the switch is turned on." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -324,24 +402,37 @@ "device_id": "[%key:common::config_flow::data::device%]", "name": "[%key:common::config_flow::data::name%]", "state": "[%key:component::template::common::state%]", - "start": "Actions on turn off", + "start": "Actions on start", "fan_speed": "Fan speed", "fan_speeds": "Fan speeds", "set_fan_speed": "Actions on set fan speed", "stop": "Actions on stop", "pause": "Actions on pause", - "return_to_base": "Actions on return to base", + "return_to_base": "Actions on return to dock", "clean_spot": "Actions on clean spot", "locate": "Actions on locate" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the vacuum. Valid values are `cleaning`, `docked`, `idle`, `paused`, `returning`, and `error`.", + "start": "Defines actions to run when the vacuum is started.", + "fan_speed": "Defines a template to get the fan speed of the vacuum.", + "fan_speeds": "List of fan speeds supported by the vacuum.", + "set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`", + "stop": "Defines actions to run when the vacuum is stopped.", + "pause": "Defines actions to run when the vacuum is paused.", + "return_to_base": "Defines actions to run when the vacuum is given a 'Return to dock' command.", + "clean_spot": "Defines actions to run when the vacuum is given a 'Clean spot' command.", + "locate": "Defines actions to run when the vacuum is given a 'Locate' command." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -366,13 +457,26 @@ "code_format": "[%key:component::template::common::code_format%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "value_template": "[%key:component::template::config::step::alarm_control_panel::data_description::value_template%]", + "disarm": "[%key:component::template::config::step::alarm_control_panel::data_description::disarm%]", + "arm_away": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_away%]", + "arm_custom_bypass": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_custom_bypass%]", + "arm_home": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_home%]", + "arm_night": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_night%]", + "arm_vacation": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_vacation%]", + "trigger": "[%key:component::template::config::step::alarm_control_panel::data_description::trigger%]", + "code_arm_required": "[%key:component::template::config::step::alarm_control_panel::data_description::code_arm_required%]", + "code_format": "[%key:component::template::config::step::alarm_control_panel::data_description::code_format%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -384,13 +488,17 @@ "state": "[%key:component::template::common::state%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::binary_sensor::data_description::state%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -402,13 +510,17 @@ "press": "[%key:component::template::config::step::button::data::press%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "press": "[%key:component::template::config::step::button::data_description::press%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -439,6 +551,9 @@ "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -468,6 +583,9 @@ "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -480,13 +598,18 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "url": "[%key:component::template::config::step::image::data_description::url%]", + "verify_ssl": "[%key:component::template::config::step::image::data_description::verify_ssl%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -507,13 +630,25 @@ "set_temperature": "[%key:component::template::config::step::light::data::set_temperature%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::light::data_description::state%]", + "turn_off": "[%key:component::template::config::step::light::data_description::turn_off%]", + "turn_on": "[%key:component::template::config::step::light::data_description::turn_on%]", + "level": "[%key:component::template::config::step::light::data_description::level%]", + "set_level": "[%key:component::template::config::step::light::data_description::set_level%]", + "hs": "[%key:component::template::config::step::light::data_description::hs%]", + "set_hs": "[%key:component::template::config::step::light::data_description::set_hs%]", + "temperature": "[%key:component::template::config::step::light::data_description::temperature%]", + "set_temperature": "[%key:component::template::config::step::light::data_description::set_temperature%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -529,13 +664,21 @@ "open": "[%key:component::template::config::step::lock::data::open%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::lock::data_description::state%]", + "lock": "[%key:component::template::config::step::lock::data_description::lock%]", + "unlock": "[%key:component::template::config::step::lock::data_description::unlock%]", + "code_format": "[%key:component::template::config::step::lock::data_description::code_format%]", + "open": "[%key:component::template::config::step::lock::data_description::open%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -552,13 +695,21 @@ "min": "[%key:component::template::config::step::number::data::min%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::number::data_description::state%]", + "step": "[%key:component::template::config::step::number::data_description::step%]", + "set_value": "[%key:component::template::config::step::number::data_description::set_value%]", + "max": "[%key:component::template::config::step::number::data_description::max%]", + "min": "[%key:component::template::config::step::number::data_description::min%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -573,13 +724,19 @@ "options": "[%key:component::template::config::step::select::data::options%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::select::data_description::state%]", + "select_option": "[%key:component::template::config::step::select::data_description::select_option%]", + "options": "[%key:component::template::config::step::select::data_description::options%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -594,13 +751,18 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::sensor::data_description::state%]", + "unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::state%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -616,13 +778,18 @@ }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", - "value_template": "[%key:component::template::config::step::switch::data_description::value_template%]" + "value_template": "[%key:component::template::config::step::switch::data_description::value_template%]", + "turn_off": "[%key:component::template::config::step::switch::data_description::turn_off%]", + "turn_on": "[%key:component::template::config::step::switch::data_description::turn_on%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -644,17 +811,30 @@ "locate": "[%key:component::template::config::step::vacuum::data::locate%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::vacuum::data_description::state%]", + "start": "[%key:component::template::config::step::vacuum::data_description::start%]", + "fan_speed": "[%key:component::template::config::step::vacuum::data_description::fan_speed%]", + "fan_speeds": "[%key:component::template::config::step::vacuum::data_description::fan_speeds%]", + "set_fan_speed": "[%key:component::template::config::step::vacuum::data_description::set_fan_speed%]", + "stop": "[%key:component::template::config::step::vacuum::data_description::stop%]", + "pause": "[%key:component::template::config::step::vacuum::data_description::pause%]", + "return_to_base": "[%key:component::template::config::step::vacuum::data_description::return_to_base%]", + "clean_spot": "[%key:component::template::config::step::vacuum::data_description::clean_spot%]", + "locate": "[%key:component::template::config::step::vacuum::data_description::locate%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, - "title": "Template vacuum" + "title": "[%key:component::template::config::step::vacuum::title%]" } } }, From 1deae3ee1a54d5755d7b41ecbecf95b369aff1f3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 30 Jul 2025 23:54:32 +0200 Subject: [PATCH 009/247] Bump reolink-aio to 0.14.5 (#149700) --- 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 39541476429a3..efd9f1121b690 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.14.4"] + "requirements": ["reolink-aio==0.14.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index f5f0c5116dc16..23ff02d69c27e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2666,7 +2666,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.4 +reolink-aio==0.14.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9336bbcc68cdf..9ede8c8f89bee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2212,7 +2212,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.4 +reolink-aio==0.14.5 # homeassistant.components.rflink rflink==0.0.67 From 918ec78348c606ec2589788a7217ec6d3787ab15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 30 Jul 2025 23:45:05 +0200 Subject: [PATCH 010/247] Add missing translations for miele dishwasher (#149702) --- homeassistant/components/miele/const.py | 10 ++++++++++ homeassistant/components/miele/strings.json | 3 +++ 2 files changed, 13 insertions(+) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index a40df909e145c..e8b626af785a8 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -431,6 +431,16 @@ class StateDryingStep(MieleEnum): 38: "quick_power_wash", 42: "tall_items", 44: "power_wash", + 200: "eco", + 202: "automatic", + 203: "comfort_wash", + 204: "power_wash", + 205: "intensive", + 207: "extra_quiet", + 209: "comfort_wash_plus", + 210: "gentle", + 214: "maintenance", + 215: "rinse_salt", } TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = { -1: "no_program", # Extrapolated from other device types. diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 01f13c8550d9f..a4400ff26ebbe 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -485,6 +485,8 @@ "cook_bacon": "Cook bacon", "biscuits_short_crust_pastry_1_tray": "Biscuits, short crust pastry (1 tray)", "biscuits_short_crust_pastry_2_trays": "Biscuits, short crust pastry (2 trays)", + "comfort_wash": "Comfort wash", + "comfort_wash_plus": "Comfort wash plus", "cool_air": "Cool air", "corn_on_the_cob": "Corn on the cob", "cottons": "Cottons", @@ -827,6 +829,7 @@ "rice_pudding_steam_cooking": "Rice pudding (steam cooking)", "rinse": "Rinse", "rinse_out_lint": "Rinse out lint", + "rinse_salt": "Rinse salt", "risotto": "Risotto", "ristretto": "Ristretto", "roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)", From d39068136029f9f764c446d8151cdb9de79cb70c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 30 Jul 2025 23:42:53 +0200 Subject: [PATCH 011/247] Fix inconsistent use of the term 'target' and a typo in MQTT translation strings (#149703) --- homeassistant/components/mqtt/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index c14bda008d1c3..40215b0f2c603 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -802,15 +802,15 @@ "data": { "max_humidity": "Maximum humidity", "min_humidity": "Minimum humidity", - "target_humidity_command_template": "Humidity command template", - "target_humidity_command_topic": "Humidity command topic", - "target_humidity_state_template": "Humidity state template", - "target_humidity_state_topic": "Humidity state topic" + "target_humidity_command_template": "Target humidity command template", + "target_humidity_command_topic": "Target humidity command topic", + "target_humidity_state_template": "Target humidity state template", + "target_humidity_state_topic": "Target humidity state topic" }, "data_description": { "max_humidity": "The maximum target humidity that can be set.", "min_humidity": "The minimum target humidity that can be set.", - "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the humidity command topic.", + "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the target humidity command topic.", "target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)", "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.", "target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)" @@ -838,7 +838,7 @@ "temperature_low_state_topic": "Lower temperature state topic" }, "data_description": { - "initial": "The climate initalizes with this target temperature.", + "initial": "The climate initializes with this target temperature.", "max_temp": "The maximum target temperature that can be set.", "min_temp": "The minimum target temperature that can be set.", "precision": "The precision in degrees the thermostat is working at.", From 21e3b8da9237d1979912081d8be5bad41f05802a Mon Sep 17 00:00:00 2001 From: Roman Sivriver Date: Wed, 30 Jul 2025 17:29:26 -0400 Subject: [PATCH 012/247] Fix typo in backup log message (#149705) --- homeassistant/components/backup/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index e7fc1262f6d48..f1b2f7d5b976d 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -1119,7 +1119,7 @@ async def _async_create_backup( ) if unavailable_agents: LOGGER.warning( - "Backup agents %s are not available, will backupp to %s", + "Backup agents %s are not available, will backup to %s", unavailable_agents, available_agents, ) From 537d09c697179ff6f951f6f1d77243df8c469c1d Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Wed, 30 Jul 2025 23:38:11 +0200 Subject: [PATCH 013/247] Fix Miele induction hob empty state (#149706) --- homeassistant/components/miele/sensor.py | 2 +- .../miele/snapshots/test_sensor.ambr | 1137 +++++++++++++++++ tests/components/miele/test_sensor.py | 15 + 3 files changed, 1153 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 216b91ca68e74..cc108841aae98 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -731,7 +731,7 @@ def native_value(self) -> StateType: ) ).name if self.device.state_plate_step - else PlatePowerStep.plate_step_0 + else PlatePowerStep.plate_step_0.name ) diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 915eda4d3615e..2805a6830773d 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1,4 +1,1141 @@ # serializer version: 1 +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:pot-steam-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_74-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction', + 'icon': 'mdi:pot-steam-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'in_use', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:pot-steam-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_74_off-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction', + 'icon': 'mdi:pot-steam-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 1', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 1', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_1_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 1', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 1', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_1_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 2', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 2', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_3', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_2_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 2', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 2', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_2_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 3', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_7', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_3_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 3', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_3_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 4', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 4', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_15', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_4_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 4', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 4', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_4_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 5', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-5', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 5', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_boost', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hood-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hood', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:turbine', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_18-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hood-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hood', + 'icon': 'mdi:turbine', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hood', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index f35404a665bdf..e5051a683c9a7 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -256,3 +256,18 @@ async def test_vacuum_sensor_states( """Test robot vacuum cleaner sensor state.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("load_device_file", ["fan_devices.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_fan_hob_sensor_states( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: None, +) -> None: + """Test robot fan / hob sensor state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) From 041c417164df759e92df35db4d86dc1a2c5320b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 31 Jul 2025 01:07:12 +0200 Subject: [PATCH 014/247] Fix bug when interpreting miele action response (#149710) --- homeassistant/components/miele/services.py | 2 +- tests/components/miele/fixtures/programs.json | 4 ++++ tests/components/miele/snapshots/test_services.ambr | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/miele/services.py b/homeassistant/components/miele/services.py index 9854196ea652d..517b489173d36 100644 --- a/homeassistant/components/miele/services.py +++ b/homeassistant/components/miele/services.py @@ -203,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse: else {} ), } - if item["parameters"] + if item.get("parameters") else {} ), } diff --git a/tests/components/miele/fixtures/programs.json b/tests/components/miele/fixtures/programs.json index ce2348f61de9d..1c232059d59a3 100644 --- a/tests/components/miele/fixtures/programs.json +++ b/tests/components/miele/fixtures/programs.json @@ -30,5 +30,9 @@ "mandatory": true } } + }, + { + "programId": 24000, + "program": "Ristretto" } ] diff --git a/tests/components/miele/snapshots/test_services.ambr b/tests/components/miele/snapshots/test_services.ambr index 3095ec9b6fbea..3c3feca783260 100644 --- a/tests/components/miele/snapshots/test_services.ambr +++ b/tests/components/miele/snapshots/test_services.ambr @@ -43,6 +43,12 @@ 'program': 'Fan plus', 'program_id': 13, }), + dict({ + 'parameters': dict({ + }), + 'program': 'Ristretto', + 'program_id': 24000, + }), ]), }) # --- From 68c43099d9dac9e4901dfe53b99c8b79dd1cb8b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Jul 2025 13:06:08 -1000 Subject: [PATCH 015/247] Fix ESPHome unnecessary probing on DHCP discovery (#149713) --- .../components/esphome/config_flow.py | 7 ++-- tests/components/esphome/test_config_flow.py | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index dc0e9b8e1b17f..4efb0e494ef96 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -316,10 +316,11 @@ async def _async_validate_mac_abort_configured( # Don't call _fetch_device_info() for ignored entries raise AbortFlow("already_configured") configured_host: str | None = entry.data.get(CONF_HOST) - configured_port: int | None = entry.data.get(CONF_PORT) - if configured_host == host and configured_port == port: + configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT) + # When port is None (from DHCP discovery), only compare hosts + if configured_host == host and (port is None or configured_port == port): # Don't probe to verify the mac is correct since - # the host and port matches. + # the host matches (and port matches if provided). raise AbortFlow("already_configured") configured_psk: str | None = entry.data.get(CONF_NOISE_PSK) await self._fetch_device_info(host, port or configured_port, configured_psk) diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index d76991a984c24..0fda7714dd01a 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -2485,3 +2485,36 @@ async def test_reconfig_name_conflict_overwrite( ) is None ) + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_discovery_dhcp_no_probe_same_host_port_none( + hass: HomeAssistant, mock_client: APIClient +) -> None: + """Test dhcp discovery does not probe when host matches and port is None.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + # DHCP discovery with same MAC and host (WiFi device) + service_info = DhcpServiceInfo( + ip="192.168.43.183", + hostname="test8266", + macaddress="11:22:33:44:55:aa", # Same MAC as configured + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Verify device_info was NOT called (no probing) + mock_client.device_info.assert_not_called() + + # Host should remain unchanged + assert entry.data[CONF_HOST] == "192.168.43.183" From ab9eebd092f15105ec06b2df3616520eecaa0cd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Jul 2025 13:54:18 -1000 Subject: [PATCH 016/247] Bump aioesphomeapi to 37.1.6 (#149715) --- 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 00d56955aa704..355089555c5cc 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.1.5", + "aioesphomeapi==37.1.6", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 23ff02d69c27e..2af9a9f712df9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.5 +aioesphomeapi==37.1.6 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ede8c8f89bee..bbfcb8b24350b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.5 +aioesphomeapi==37.1.6 # homeassistant.components.flo aioflo==2021.11.0 From bd0a3f5a5dc2dde9cca4dd3e244c1e741d8fc73f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 00:10:23 -1000 Subject: [PATCH 017/247] Bump aioesphomeapi to 37.2.0 (#149732) --- 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 355089555c5cc..5a7c9a5f92773 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.1.6", + "aioesphomeapi==37.2.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 2af9a9f712df9..dc1f8c0f2ac40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.6 +aioesphomeapi==37.2.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbfcb8b24350b..765719cb55c03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.6 +aioesphomeapi==37.2.0 # homeassistant.components.flo aioflo==2021.11.0 From f5f63b914a4f77875f26056bb13f03249ff7798a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Jul 2025 12:35:13 +0200 Subject: [PATCH 018/247] Make _EventDeviceRegistryUpdatedData_Remove JSON serializable (#149734) --- homeassistant/helpers/device_registry.py | 4 ++-- homeassistant/helpers/entity_registry.py | 6 +++--- tests/helpers/test_device_registry.py | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index bc6e7c810bf54..c8b4428a7cc2c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -156,7 +156,7 @@ class _EventDeviceRegistryUpdatedData_Remove(TypedDict): action: Literal["remove"] device_id: str - device: DeviceEntry + device: dict[str, Any] class _EventDeviceRegistryUpdatedData_Update(TypedDict): @@ -1319,7 +1319,7 @@ def async_remove_device(self, device_id: str) -> None: self.hass.bus.async_fire_internal( EVENT_DEVICE_REGISTRY_UPDATED, _EventDeviceRegistryUpdatedData_Remove( - action="remove", device_id=device_id, device=device + action="remove", device_id=device_id, device=device.dict_repr ), ) self.async_schedule_save() diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7051521b80546..d972b421fc43b 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1103,13 +1103,13 @@ def async_device_modified( entities = async_entries_for_device( self, event.data["device_id"], include_disabled_entities=True ) - removed_device = event.data["device"] + removed_device_dict = event.data["device"] for entity in entities: config_entry_id = entity.config_entry_id if ( - config_entry_id in removed_device.config_entries + config_entry_id in removed_device_dict["config_entries"] and entity.config_subentry_id - in removed_device.config_entries_subentries[config_entry_id] + in removed_device_dict["config_entries_subentries"][config_entry_id] ): self.async_remove(entity.entity_id) else: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 23a451dd06cef..a66684c94e37a 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1652,7 +1652,7 @@ async def test_removing_config_entries( assert update_events[4].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } @@ -1725,12 +1725,12 @@ async def test_deleted_device_removing_config_entries( assert update_events[3].data == { "action": "remove", "device_id": entry.id, - "device": entry2, + "device": entry2.dict_repr, } assert update_events[4].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } device_registry.async_clear_config_entry(config_entry_1.entry_id) @@ -1976,7 +1976,7 @@ async def test_removing_config_subentries( assert update_events[7].data == { "action": "remove", "device_id": entry.id, - "device": entry, + "device": entry.dict_repr, } @@ -2106,7 +2106,7 @@ async def test_deleted_device_removing_config_subentries( assert update_events[4].data == { "action": "remove", "device_id": entry.id, - "device": entry4, + "device": entry4.dict_repr, } device_registry.async_clear_config_subentry(config_entry_1.entry_id, None) @@ -2930,7 +2930,7 @@ async def test_update_remove_config_entries( assert update_events[6].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } @@ -3208,7 +3208,7 @@ async def test_update_remove_config_subentries( assert update_events[7].data == { "action": "remove", "device_id": entry_id, - "device": entry_before_remove, + "device": entry_before_remove.dict_repr, } @@ -3551,7 +3551,7 @@ async def test_restore_device( assert update_events[2].data == { "action": "remove", "device_id": entry.id, - "device": entry, + "device": entry.dict_repr, } assert update_events[3].data == { "action": "create", @@ -3874,7 +3874,7 @@ async def test_restore_shared_device( assert update_events[3].data == { "action": "remove", "device_id": entry.id, - "device": updated_device, + "device": updated_device.dict_repr, } assert update_events[4].data == { "action": "create", @@ -3883,7 +3883,7 @@ async def test_restore_shared_device( assert update_events[5].data == { "action": "remove", "device_id": entry.id, - "device": entry2, + "device": entry2.dict_repr, } assert update_events[6].data == { "action": "create", From 3ccb7deb3c1e64388a5ac7139be75bd6a89e781b Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:19:43 -0400 Subject: [PATCH 019/247] Nitpick default translations for template integration (#149740) --- homeassistant/components/template/strings.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index b412fa519cde6..d29bfbeb3fb85 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -39,7 +39,7 @@ "arm_vacation": "Defines actions to run when the alarm control panel is armed to `arm_vacation`. Receives variable `code`.", "trigger": "Defines actions to run when the alarm control panel is triggered. Receives variable `code`.", "code_arm_required": "If true, the code is required to arm the alarm.", - "code_format": "One of number, text or no_code. Format for the code used to arm/disarm the alarm." + "code_format": "One of `number`, `text` or `no_code`. Format for the code used to arm/disarm the alarm." }, "sections": { "advanced_options": { @@ -179,7 +179,7 @@ "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", "url": "Defines a template to get the URL on which the image is served.", - "verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http-only URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification." + "verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification." }, "sections": { "advanced_options": { @@ -282,7 +282,7 @@ "set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.", "max": "Template for the number's maximum value.", "min": "Template for the number's minimum value.", - "unit_of_measurement": "Defines the units of measurement of the number, if any." + "unit_of_measurement": "Defines the unit of measurement of the number, if any." }, "sections": { "advanced_options": { @@ -336,7 +336,7 @@ "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", "state": "Defines a template to get the state of the sensor. If the sensor is numeric, i.e. it has a `state_class` or a `unit_of_measurement`, the state template must render to a number or to `none`. The state template must not render to a string, including `unknown` or `unavailable`. An `availability` template may be defined to suppress rendering of the state template.", - "unit_of_measurement": "Defines the units of measurement of the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value." + "unit_of_measurement": "Defines the unit of measurement for the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value." }, "sections": { "advanced_options": { @@ -418,7 +418,7 @@ "start": "Defines actions to run when the vacuum is started.", "fan_speed": "Defines a template to get the fan speed of the vacuum.", "fan_speeds": "List of fan speeds supported by the vacuum.", - "set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`", + "set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`.", "stop": "Defines actions to run when the vacuum is stopped.", "pause": "Defines actions to run when the vacuum is paused.", "return_to_base": "Defines actions to run when the vacuum is given a 'Return to dock' command.", From 3fc6ebdb4397b4e96a490ef5c9150f68058a804a Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:19:37 -0400 Subject: [PATCH 020/247] Fix unique_id in config validation for legacy weather platform (#149742) --- homeassistant/components/template/weather.py | 2 ++ tests/components/template/test_weather.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 7f79adc220164..bddb55197c393 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -34,6 +34,7 @@ from homeassistant.const import ( CONF_NAME, CONF_TEMPERATURE_UNIT, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -151,6 +152,7 @@ vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS), vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS), vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template, diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index 6e2a2ab2f6b50..7eac7ff28aa68 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -132,6 +132,7 @@ async def setup_weather( { "platform": "template", "name": "test", + "unique_id": "abc123", "attribution_template": "{{ states('sensor.attribution') }}", "condition_template": "sunny", "temperature_template": "{{ states('sensor.temperature') | float }}", From fc04e0b2cca9f1e3b7b193998280fbb80e10b693 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 31 Jul 2025 18:46:10 +0200 Subject: [PATCH 021/247] Update frontend to 20250731.0 (#149757) --- 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 09461a3543a0d..706940f5da7a2 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==20250730.0"] + "requirements": ["home-assistant-frontend==20250731.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 704fb282784e6..cd0fc31b00875 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==4.0.1 hass-nabucasa==0.110.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250730.0 +home-assistant-frontend==20250731.0 home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index dc1f8c0f2ac40..eb2d44e24f698 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250730.0 +home-assistant-frontend==20250731.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 765719cb55c03..9ff3286b03b15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250730.0 +home-assistant-frontend==20250731.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 From 22214e8d3120eaf42239badd7f7de29f181d98c7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Jul 2025 17:49:09 +0200 Subject: [PATCH 022/247] Fix kitchen_sink option flow (#149760) --- homeassistant/components/kitchen_sink/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py index 059fd11999f8c..056ace7011cf1 100644 --- a/homeassistant/components/kitchen_sink/config_flow.py +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -99,7 +99,7 @@ async def async_step_options_1( ), } ) - self.add_suggested_values_to_schema( + data_schema = self.add_suggested_values_to_schema( data_schema, {"section_1": {"int": self.config_entry.options.get(CONF_INT, 10)}}, ) From 15cb48badb50a534571f837ae7bac98b6122cee4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 31 Jul 2025 19:01:26 +0200 Subject: [PATCH 023/247] Bump version to 2025.8.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 97e463f851ead..596a99afb9272 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 2fee88acceeb1..e454bdde6ab11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0b0" +version = "2025.8.0b1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 38d0ebb8ba628558fc74c2106d03a60fc2651e8f Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 1 Aug 2025 09:24:22 +0200 Subject: [PATCH 024/247] Add diagnostics to UISP AirOS (#149631) --- homeassistant/components/airos/diagnostics.py | 33 + .../components/airos/quality_scale.yaml | 2 +- .../airos/snapshots/test_diagnostics.ambr | 623 ++++++++++++++++++ tests/components/airos/test_diagnostics.py | 32 + 4 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airos/diagnostics.py create mode 100644 tests/components/airos/snapshots/test_diagnostics.ambr create mode 100644 tests/components/airos/test_diagnostics.py diff --git a/homeassistant/components/airos/diagnostics.py b/homeassistant/components/airos/diagnostics.py new file mode 100644 index 0000000000000..70fef685c8689 --- /dev/null +++ b/homeassistant/components/airos/diagnostics.py @@ -0,0 +1,33 @@ +"""Diagnostics support for airOS.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from .coordinator import AirOSConfigEntry + +IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related +HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address +TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD] +TO_REDACT_AIROS = [ + "hostname", # Prevent leaking device naming + "essid", # Network SSID + "lat", # GPS latitude to prevent exposing location data. + "lon", # GPS longitude to prevent exposing location data. + *HW_REDACT, + *IP_REDACT, +] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: AirOSConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return { + "entry_data": async_redact_data(entry.data, TO_REDACT_HA), + "data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS), + } diff --git a/homeassistant/components/airos/quality_scale.yaml b/homeassistant/components/airos/quality_scale.yaml index a0bacd5ebba30..c8c5d209af5c3 100644 --- a/homeassistant/components/airos/quality_scale.yaml +++ b/homeassistant/components/airos/quality_scale.yaml @@ -41,7 +41,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: todo discovery: todo docs-data-update: done diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr new file mode 100644 index 0000000000000..bc2dedc905aa6 --- /dev/null +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -0,0 +1,623 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'chain_names': list([ + dict({ + 'name': 'Chain 0', + 'number': 1, + }), + dict({ + 'name': 'Chain 1', + 'number': 2, + }), + ]), + 'derived': dict({ + 'mac': '**REDACTED**', + 'mac_interface': 'br0', + }), + 'firewall': dict({ + 'eb6tables': False, + 'ebtables': False, + 'ip6tables': False, + 'iptables': False, + }), + 'genuine': '/images/genuine.png', + 'gps': dict({ + 'fix': 0, + 'lat': '**REDACTED**', + 'lon': '**REDACTED**', + }), + 'host': dict({ + 'cpuload': 10.10101, + 'device_id': '03aa0d0b40fed0a47088293584ef5432', + 'devmodel': 'NanoStation 5AC loco', + 'freeram': 16564224, + 'fwversion': 'v8.7.17', + 'height': 3, + 'hostname': '**REDACTED**', + 'loadavg': 0.412598, + 'netrole': 'bridge', + 'power_time': 268683, + 'temperature': 0, + 'time': '2025-06-23 23:06:42', + 'timestamp': 2668313184, + 'totalram': 63447040, + 'uptime': 264888, + }), + 'interfaces': list([ + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'eth0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': 18, + 'duplex': True, + 'ip6addr': None, + 'ipaddr': '**REDACTED**', + 'plugged': True, + 'rx_bytes': 3984971949, + 'rx_dropped': 0, + 'rx_errors': 4, + 'rx_packets': 73564835, + 'snr': list([ + 30, + 30, + 30, + 30, + ]), + 'speed': 1000, + 'tx_bytes': 209900085624, + 'tx_dropped': 10, + 'tx_errors': 0, + 'tx_packets': 185866883, + }), + }), + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'ath0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': None, + 'duplex': False, + 'ip6addr': None, + 'ipaddr': '**REDACTED**', + 'plugged': False, + 'rx_bytes': 206938324766, + 'rx_dropped': 0, + 'rx_errors': 0, + 'rx_packets': 149767200, + 'snr': None, + 'speed': 0, + 'tx_bytes': 5265602738, + 'tx_dropped': 2005, + 'tx_errors': 0, + 'tx_packets': 52980390, + }), + }), + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'br0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': None, + 'duplex': False, + 'ip6addr': '**REDACTED**', + 'ipaddr': '**REDACTED**', + 'plugged': True, + 'rx_bytes': 204802727, + 'rx_dropped': 0, + 'rx_errors': 0, + 'rx_packets': 1791592, + 'snr': None, + 'speed': 0, + 'tx_bytes': 236295176, + 'tx_dropped': 0, + 'tx_errors': 0, + 'tx_packets': 298119, + }), + }), + ]), + 'ntpclient': dict({ + }), + 'portfw': False, + 'provmode': dict({ + }), + 'services': dict({ + 'airview': 2, + 'dhcp6d_stateful': False, + 'dhcpc': False, + 'dhcpd': False, + 'pppoe': False, + }), + 'unms': dict({ + 'status': 0, + 'timestamp': None, + }), + 'wireless': dict({ + 'antenna_gain': 13, + 'apmac': '**REDACTED**', + 'aprepeater': False, + 'band': 2, + 'cac_state': 0, + 'cac_timeout': 0, + 'center1_freq': 5530, + 'chanbw': 80, + 'compat_11n': 0, + 'count': 1, + 'dfs': 1, + 'distance': 0, + 'essid': '**REDACTED**', + 'frequency': 5500, + 'hide_essid': 0, + 'ieeemode': '11ACVHT80', + 'mode': 'ap-ptp', + 'noisef': -89, + 'nol_state': 0, + 'nol_timeout': 0, + 'polling': dict({ + 'atpc_status': 2, + 'cb_capacity': 593970, + 'dl_capacity': 647400, + 'ff_cap_rep': False, + 'fixed_frame': False, + 'gps_sync': False, + 'rx_use': 42, + 'tx_use': 6, + 'ul_capacity': 540540, + 'use': 48, + }), + 'rstatus': 5, + 'rx_chainmask': 3, + 'rx_idx': 8, + 'rx_nss': 2, + 'security': 'WPA2', + 'service': dict({ + 'link': 266003, + 'time': 267181, + }), + 'sta': list([ + dict({ + 'airmax': dict({ + 'actual_priority': 0, + 'atpc_status': 2, + 'beam': 0, + 'cb_capacity': 593970, + 'desired_priority': 0, + 'dl_capacity': 647400, + 'rx': dict({ + 'cinr': 31, + 'evm': list([ + list([ + 31, + 28, + 33, + 32, + 32, + 32, + 31, + 31, + 31, + 29, + 30, + 32, + 30, + 27, + 34, + 31, + 31, + 30, + 32, + 29, + 31, + 29, + 31, + 33, + 31, + 31, + 32, + 30, + 31, + 34, + 33, + 31, + 30, + 31, + 30, + 31, + 31, + 32, + 31, + 30, + 33, + 31, + 30, + 31, + 27, + 31, + 30, + 30, + 30, + 30, + 30, + 29, + 32, + 34, + 31, + 30, + 28, + 30, + 29, + 35, + 31, + 33, + 32, + 29, + ]), + list([ + 34, + 34, + 35, + 34, + 35, + 35, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + 34, + 34, + 35, + 34, + 33, + 33, + 35, + 34, + 34, + 35, + 34, + 35, + 34, + 34, + 35, + 34, + 34, + 33, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + 35, + 34, + 35, + 33, + 34, + 34, + 34, + 34, + 35, + 35, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + ]), + ]), + 'usage': 42, + }), + 'tx': dict({ + 'cinr': 31, + 'evm': list([ + list([ + 32, + 34, + 28, + 33, + 35, + 30, + 31, + 33, + 30, + 30, + 32, + 30, + 29, + 33, + 31, + 29, + 33, + 31, + 31, + 30, + 33, + 34, + 33, + 31, + 33, + 32, + 32, + 31, + 29, + 31, + 30, + 32, + 31, + 30, + 29, + 32, + 31, + 32, + 31, + 31, + 32, + 29, + 31, + 29, + 30, + 32, + 32, + 31, + 32, + 32, + 33, + 31, + 28, + 29, + 31, + 31, + 33, + 32, + 33, + 32, + 32, + 32, + 31, + 33, + ]), + list([ + 37, + 37, + 37, + 38, + 38, + 37, + 36, + 38, + 38, + 37, + 37, + 37, + 37, + 37, + 39, + 37, + 37, + 37, + 37, + 37, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 37, + 38, + 37, + 37, + 38, + 37, + 37, + 37, + 38, + 37, + 38, + 37, + 37, + 37, + 37, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 38, + 37, + 37, + 38, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 37, + ]), + ]), + 'usage': 6, + }), + 'ul_capacity': 540540, + }), + 'airos_connected': True, + 'cb_capacity_expect': 416000, + 'chainrssi': list([ + 35, + 32, + 0, + ]), + 'distance': 1, + 'dl_avg_linkscore': 100, + 'dl_capacity_expect': 208000, + 'dl_linkscore': 100, + 'dl_rate_expect': 3, + 'dl_signal_expect': -80, + 'last_disc': 1, + 'lastip': '**REDACTED**', + 'mac': '**REDACTED**', + 'noisefloor': -89, + 'remote': dict({ + 'age': 1, + 'airview': 2, + 'antenna_gain': 13, + 'cable_loss': 0, + 'chainrssi': list([ + 33, + 37, + 0, + ]), + 'compat_11n': 0, + 'cpuload': 43.564301, + 'device_id': 'd4f4cdf82961e619328a8f72f8d7653b', + 'distance': 1, + 'ethlist': list([ + dict({ + 'cable_len': 14, + 'duplex': True, + 'enabled': True, + 'ifname': 'eth0', + 'plugged': True, + 'snr': list([ + 30, + 30, + 29, + 30, + ]), + 'speed': 1000, + }), + ]), + 'freeram': 14290944, + 'gps': dict({ + 'fix': 0, + 'lat': '**REDACTED**', + 'lon': '**REDACTED**', + }), + 'height': 2, + 'hostname': '**REDACTED**', + 'ip6addr': '**REDACTED**', + 'ipaddr': '**REDACTED**', + 'mode': 'sta-ptp', + 'netrole': 'bridge', + 'noisefloor': -90, + 'oob': False, + 'platform': 'NanoStation 5AC loco', + 'power_time': 268512, + 'rssi': 38, + 'rx_bytes': 3624206478, + 'rx_chainmask': 3, + 'rx_throughput': 251, + 'service': dict({ + 'link': 265996, + 'time': 267195, + }), + 'signal': -58, + 'sys_id': '0xe7fa', + 'temperature': 0, + 'time': '2025-06-23 23:13:54', + 'totalram': 63447040, + 'tx_bytes': 212308148210, + 'tx_power': -4, + 'tx_ratedata': list([ + 14, + 4, + 372, + 2223, + 4708, + 4037, + 8142, + 485763, + 29420892, + 24748154, + ]), + 'tx_throughput': 16023, + 'unms': dict({ + 'status': 0, + 'timestamp': None, + }), + 'uptime': 265320, + 'version': 'WA.ar934x.v8.7.17.48152.250620.2132', + }), + 'rssi': 37, + 'rx_idx': 8, + 'rx_nss': 2, + 'signal': -59, + 'stats': dict({ + 'rx_bytes': 206938324814, + 'rx_packets': 149767200, + 'rx_pps': 846, + 'tx_bytes': 5265602739, + 'tx_packets': 52980390, + 'tx_pps': 0, + }), + 'tx_idx': 9, + 'tx_latency': 0, + 'tx_lretries': 0, + 'tx_nss': 2, + 'tx_packets': 0, + 'tx_ratedata': list([ + 175, + 4, + 47, + 200, + 673, + 158, + 163, + 138, + 68895, + 19577430, + ]), + 'tx_sretries': 0, + 'ul_avg_linkscore': 88, + 'ul_capacity_expect': 624000, + 'ul_linkscore': 86, + 'ul_rate_expect': 8, + 'ul_signal_expect': -55, + 'uptime': 170281, + }), + ]), + 'sta_disconnected': list([ + ]), + 'throughput': dict({ + 'rx': 9907, + 'tx': 222, + }), + 'tx_chainmask': 3, + 'tx_idx': 9, + 'tx_nss': 2, + 'txpower': -3, + }), + }), + 'entry_data': dict({ + 'host': '**REDACTED**', + 'password': '**REDACTED**', + 'username': 'ubnt', + }), + }) +# --- diff --git a/tests/components/airos/test_diagnostics.py b/tests/components/airos/test_diagnostics.py new file mode 100644 index 0000000000000..453e8ff1f0355 --- /dev/null +++ b/tests/components/airos/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Diagnostic tests for airOS.""" + +from unittest.mock import MagicMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.airos.coordinator import AirOSData +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_airos_client: MagicMock, + mock_config_entry: MockConfigEntry, + ap_fixture: AirOSData, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + await setup_integration(hass, mock_config_entry) + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) From 70e54fdaddf235a34d94ae077f43d5b5dba4a853 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Jul 2025 17:55:15 +0200 Subject: [PATCH 025/247] Improve test of FlowHandler.add_suggested_values_to_schema (#149759) --- tests/test_data_entry_flow.py | 42 ++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index a5908f0feabf6..fc40a330a1a36 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -155,19 +155,23 @@ class TestFlow(data_entry_flow.FlowHandler): async def async_step_init(self, user_input=None): data_schema = self.add_suggested_values_to_schema( schema, - { - "username": "doej", - "password": "verySecret1", - "section_1": {"full_name": "John Doe"}, - }, + user_input, ) return self.async_show_form( step_id="init", data_schema=data_schema, ) - form = await manager.async_init("test") + form = await manager.async_init( + "test", + data={ + "username": "doej", + "password": "verySecret1", + "section_1": {"full_name": "John Doe"}, + }, + ) assert form["type"] == data_entry_flow.FlowResultType.FORM + assert form["data_schema"].schema is not schema.schema assert form["data_schema"].schema == schema.schema markers = list(form["data_schema"].schema) assert len(markers) == 3 @@ -187,6 +191,32 @@ async def async_step_init(self, user_input=None): assert section_markers[0] == "full_name" assert section_markers[0].description == {"suggested_value": "John Doe"} + # Test again without suggested values to make sure we're not mutating the schema + form = await manager.async_init( + "test", + ) + assert form["type"] == data_entry_flow.FlowResultType.FORM + assert form["data_schema"].schema is not schema.schema + assert form["data_schema"].schema == schema.schema + markers = list(form["data_schema"].schema) + assert len(markers) == 3 + assert markers[0] == "username" + assert markers[0].description is None + assert markers[1] == "password" + assert markers[1].description is None + assert markers[2] == "section_1" + section_validator = form["data_schema"].schema["section_1"] + assert isinstance(section_validator, data_entry_flow.section) + # The section class was not replaced + assert section_validator is schema.schema["section_1"] + # The section schema was not replaced + assert section_validator.schema is schema.schema["section_1"].schema + section_markers = list(section_validator.schema.schema) + assert len(section_markers) == 1 + assert section_markers[0] == "full_name" + # This is a known bug, which needs to be fixed + assert section_markers[0].description == {"suggested_value": "John Doe"} + async def test_abort_removes_instance(manager: MockFlowManager) -> None: """Test that abort removes the flow from progress.""" From 1662d36125d4997fd77d99728d159fdaa5b31dd1 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:50:26 -0700 Subject: [PATCH 026/247] Fix `add_suggested_values_to_schema` when the schema has sections (#149718) Co-authored-by: Erik Montnemery --- homeassistant/data_entry_flow.py | 7 ++++--- tests/test_data_entry_flow.py | 32 +++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index ce1c0806b14d9..32900c2a24772 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -677,9 +677,10 @@ def add_suggested_values_to_schema( and key in suggested_values ): new_section_key = copy.copy(key) - schema[new_section_key] = val - val.schema = self.add_suggested_values_to_schema( - val.schema, suggested_values[key] + new_val = copy.copy(val) + schema[new_section_key] = new_val + new_val.schema = self.add_suggested_values_to_schema( + new_val.schema, suggested_values[key] ) continue diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index fc40a330a1a36..0faa4dd1a80aa 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -135,6 +135,19 @@ async def async_step_init(self, user_input=None): async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) -> None: """Test that we can show a form with suggested values.""" + + def compare_schemas(schema: vol.Schema, expected_schema: vol.Schema) -> None: + """Compare two schemas.""" + assert schema.schema is not expected_schema.schema + + assert list(schema.schema) == list(expected_schema.schema) + + for key, validator in schema.schema.items(): + if isinstance(validator, data_entry_flow.section): + assert validator.schema == expected_schema.schema[key].schema + continue + assert validator == expected_schema.schema[key] + schema = vol.Schema( { vol.Required("username"): str, @@ -172,7 +185,8 @@ async def async_step_init(self, user_input=None): ) assert form["type"] == data_entry_flow.FlowResultType.FORM assert form["data_schema"].schema is not schema.schema - assert form["data_schema"].schema == schema.schema + assert form["data_schema"].schema != schema.schema + compare_schemas(form["data_schema"], schema) markers = list(form["data_schema"].schema) assert len(markers) == 3 assert markers[0] == "username" @@ -182,10 +196,11 @@ async def async_step_init(self, user_input=None): assert markers[2] == "section_1" section_validator = form["data_schema"].schema["section_1"] assert isinstance(section_validator, data_entry_flow.section) - # The section class was not replaced - assert section_validator is schema.schema["section_1"] - # The section schema was not replaced - assert section_validator.schema is schema.schema["section_1"].schema + # The section instance was copied + assert section_validator is not schema.schema["section_1"] + # The section schema instance was copied + assert section_validator.schema is not schema.schema["section_1"].schema + assert section_validator.schema == schema.schema["section_1"].schema section_markers = list(section_validator.schema.schema) assert len(section_markers) == 1 assert section_markers[0] == "full_name" @@ -207,15 +222,14 @@ async def async_step_init(self, user_input=None): assert markers[2] == "section_1" section_validator = form["data_schema"].schema["section_1"] assert isinstance(section_validator, data_entry_flow.section) - # The section class was not replaced + # The section class is not replaced if there is no suggested value for the section assert section_validator is schema.schema["section_1"] - # The section schema was not replaced + # The section schema is not replaced if there is no suggested value for the section assert section_validator.schema is schema.schema["section_1"].schema section_markers = list(section_validator.schema.schema) assert len(section_markers) == 1 assert section_markers[0] == "full_name" - # This is a known bug, which needs to be fixed - assert section_markers[0].description == {"suggested_value": "John Doe"} + assert section_markers[0].description is None async def test_abort_removes_instance(manager: MockFlowManager) -> None: From 9435b0ad3af5e751dae90b457363e9f99209d40c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Aug 2025 07:49:51 +0200 Subject: [PATCH 027/247] Fix flaky velbus test (#149743) --- tests/components/velbus/conftest.py | 5 +- .../velbus/snapshots/test_init.ambr | 106 ++++++++++++++---- tests/components/velbus/test_init.py | 3 +- 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index f7cbeb7a0528d..d909480c8ea0b 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -97,6 +97,7 @@ def mock_module_subdevices() -> AsyncMock: """Mock a velbus module.""" module = AsyncMock(spec=Module) module.get_type_name.return_value = "VMB2BLE" + module.get_type.return_value = "123" module.get_addresses.return_value = [88] module.get_name.return_value = "Kitchen" module.get_serial.return_value = "a1b2c3d4e5f6" @@ -138,7 +139,7 @@ def mock_temperature() -> AsyncMock: channel.get_module_sw_version.return_value = "3.0.0" channel.get_module_serial.return_value = "asdfghjk" channel.get_module_type.return_value = 1 - channel.is_sub_device.return_value = False + channel.is_sub_device.return_value = True channel.is_counter_channel.return_value = False channel.get_class.return_value = "temperature" channel.get_unit.return_value = "°C" @@ -184,7 +185,7 @@ def mock_select() -> AsyncMock: channel.get_full_name.return_value = "Kitchen" channel.get_module_sw_version.return_value = "1.1.1" channel.get_module_serial.return_value = "qwerty1234567" - channel.is_sub_device.return_value = False + channel.is_sub_device.return_value = True channel.get_options.return_value = ["none", "summer", "winter", "holiday"] channel.get_selected_program.return_value = "winter" return channel diff --git a/tests/components/velbus/snapshots/test_init.ambr b/tests/components/velbus/snapshots/test_init.ambr index 1e17753a02ff9..037ab7e62365d 100644 --- a/tests/components/velbus/snapshots/test_init.ambr +++ b/tests/components/velbus/snapshots/test_init.ambr @@ -46,22 +46,22 @@ 'identifiers': set({ tuple( 'velbus', - '88-9', + '2', ), }), 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', - 'model': 'VMB2BLE', - 'model_id': '10', - 'name': 'Basement', + 'model': 'VMB7IN', + 'model_id': '8', + 'name': 'Input', 'name_by_user': None, 'primary_config_entry': , - 'serial_number': '1234', + 'serial_number': 'a1b2c3d4e5f6', 'suggested_area': None, - 'sw_version': '1.0.1', - 'via_device_id': , + 'sw_version': '1.0.0', + 'via_device_id': None, }), DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -77,7 +77,7 @@ 'identifiers': set({ tuple( 'velbus', - '88-11', + '88', ), }), 'is_new': False, @@ -85,14 +85,14 @@ }), 'manufacturer': 'Velleman', 'model': 'VMB2BLE', - 'model_id': '10', - 'name': 'Basement', + 'model_id': '123', + 'name': 'Kitchen (VMB2BLE)', 'name_by_user': None, 'primary_config_entry': , - 'serial_number': '12345', + 'serial_number': 'a1b2c3d4e5f6', 'suggested_area': None, - 'sw_version': '1.0.1', - 'via_device_id': , + 'sw_version': '2.0.0', + 'via_device_id': None, }), DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -125,6 +125,37 @@ 'sw_version': '1.0.0', 'via_device_id': , }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-11', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB2BLE', + 'model_id': '10', + 'name': 'Basement', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '12345', + 'suggested_area': None, + 'sw_version': '1.0.1', + 'via_device_id': , + }), DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -170,7 +201,7 @@ 'identifiers': set({ tuple( 'velbus', - '88', + '88-3', ), }), 'is_new': False, @@ -185,7 +216,7 @@ 'serial_number': 'asdfghjk', 'suggested_area': None, 'sw_version': '3.0.0', - 'via_device_id': None, + 'via_device_id': , }), DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -201,22 +232,22 @@ 'identifiers': set({ tuple( 'velbus', - '2', + '88-33', ), }), 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', - 'model': 'VMB7IN', - 'model_id': '8', - 'name': 'Input', + 'model': 'VMB4RYNO', + 'model_id': '3', + 'name': 'Kitchen', 'name_by_user': None, 'primary_config_entry': , - 'serial_number': 'a1b2c3d4e5f6', + 'serial_number': 'qwerty1234567', 'suggested_area': None, - 'sw_version': '1.0.0', - 'via_device_id': None, + 'sw_version': '1.1.1', + 'via_device_id': , }), DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -249,5 +280,36 @@ 'sw_version': '1.0.1', 'via_device_id': , }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-9', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB2BLE', + 'model_id': '10', + 'name': 'Basement', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '1234', + 'suggested_area': None, + 'sw_version': '1.0.1', + 'via_device_id': , + }), ]) # --- diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index 2d28ba81cb146..fc9046f977fdf 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -176,7 +176,8 @@ async def test_device_registry( device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) - assert device_entries == snapshot + # Sort by identifier to ensure consistent order in snapshot + assert sorted(device_entries, key=lambda x: list(x.identifiers)[0][1]) == snapshot device_parent = device_registry.async_get_device(identifiers={(DOMAIN, "88")}) assert device_parent.via_device_id is None From 073589ae19e834104c81f2d56ef5bca9aaf5e927 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Aug 2025 10:34:34 +0200 Subject: [PATCH 028/247] Deprecate DeviceEntry.suggested_area (#149730) --- .../components/analytics/analytics.py | 1 - .../components/enphase_envoy/diagnostics.py | 2 + homeassistant/helpers/device_registry.py | 16 +- .../components/acaia/snapshots/test_init.ambr | 1 - .../airgradient/snapshots/test_init.ambr | 2 - .../alexa_devices/snapshots/test_init.ambr | 1 - tests/components/analytics/test_analytics.py | 2 - .../aosmith/snapshots/test_device.ambr | 1 - .../apcupsd/snapshots/test_init.ambr | 4 - .../august/snapshots/test_binary_sensor.ambr | 1 - .../august/snapshots/test_lock.ambr | 1 - tests/components/axis/snapshots/test_hub.ambr | 2 - tests/components/bond/test_init.py | 18 +- .../cambridge_audio/snapshots/test_init.ambr | 1 - .../components/deconz/snapshots/test_hub.ambr | 1 - .../snapshots/test_init.ambr | 3 - .../ecovacs/snapshots/test_init.ambr | 1 - .../elgato/snapshots/test_button.ambr | 2 - .../elgato/snapshots/test_light.ambr | 3 - .../elgato/snapshots/test_sensor.ambr | 5 - .../elgato/snapshots/test_switch.ambr | 2 - .../snapshots/test_diagnostics.ambr | 8 - tests/components/esphome/test_manager.py | 35 ++- tests/components/flo/snapshots/test_init.ambr | 2 - .../snapshots/test_init.ambr | 1 - .../snapshots/test_init.ambr | 4 - .../components/homee/snapshots/test_init.ambr | 2 - .../snapshots/test_init.ambr | 116 --------- .../homekit_controller/test_init.py | 2 + .../homewizard/snapshots/test_button.ambr | 1 - .../homewizard/snapshots/test_number.ambr | 2 - .../homewizard/snapshots/test_select.ambr | 1 - .../homewizard/snapshots/test_sensor.ambr | 231 ------------------ .../homewizard/snapshots/test_switch.ambr | 11 - tests/components/hue/test_light_v1.py | 21 +- .../snapshots/test_init.ambr | 1 - .../snapshots/test_init.ambr | 1 - .../iotty/snapshots/test_switch.ambr | 1 - .../ista_ecotrend/snapshots/test_init.ambr | 2 - .../ituran/snapshots/test_init.ambr | 1 - .../kitchen_sink/snapshots/test_switch.ambr | 4 - .../lamarzocco/snapshots/test_init.ambr | 1 - .../lektrico/snapshots/test_init.ambr | 1 - tests/components/lifx/test_config_flow.py | 9 +- .../mastodon/snapshots/test_init.ambr | 1 - .../mealie/snapshots/test_init.ambr | 1 - .../meater/snapshots/test_init.ambr | 1 - .../components/miele/snapshots/test_init.ambr | 1 - tests/components/mqtt/common.py | 20 +- .../myuplink/snapshots/test_init.ambr | 3 - .../netatmo/snapshots/test_init.ambr | 39 --- .../netgear_lte/snapshots/test_init.ambr | 1 - tests/components/nut/test_init.py | 8 +- .../nyt_games/snapshots/test_init.ambr | 3 - .../components/ohme/snapshots/test_init.ambr | 1 - .../ondilo_ico/snapshots/test_init.ambr | 2 - .../onedrive/snapshots/test_init.ambr | 1 - .../onewire/snapshots/test_init.ambr | 22 -- .../snapshots/test_init.ambr | 2 - .../overseerr/snapshots/test_init.ambr | 1 - .../palazzetti/snapshots/test_init.ambr | 1 - .../peblar/snapshots/test_init.ambr | 1 - .../rainforest_raven/snapshots/test_init.ambr | 1 - .../renault/snapshots/test_init.ambr | 5 - tests/components/roku/test_binary_sensor.py | 13 +- tests/components/roku/test_media_player.py | 13 +- tests/components/roku/test_sensor.py | 13 +- .../components/rova/snapshots/test_init.ambr | 1 - .../russound_rio/snapshots/test_init.ambr | 1 - .../samsungtv/snapshots/test_init.ambr | 3 - .../schlage/snapshots/test_init.ambr | 1 - .../sensibo/snapshots/test_entity.ambr | 4 - .../sfr_box/snapshots/test_binary_sensor.ambr | 2 - .../sfr_box/snapshots/test_button.ambr | 1 - .../sfr_box/snapshots/test_sensor.ambr | 1 - .../slide_local/snapshots/test_init.ambr | 1 - .../smartthings/snapshots/test_init.ambr | 68 ------ .../smarty/snapshots/test_init.ambr | 1 - .../smlight/snapshots/test_init.ambr | 1 - tests/components/sonos/test_media_player.py | 12 +- .../squeezebox/snapshots/test_init.ambr | 2 - .../snapshots/test_binary_sensor.ambr | 2 - .../tailwind/snapshots/test_button.ambr | 1 - .../tailwind/snapshots/test_cover.ambr | 2 - .../tailwind/snapshots/test_number.ambr | 1 - .../components/tedee/snapshots/test_init.ambr | 2 - .../components/tedee/snapshots/test_lock.ambr | 1 - .../tesla_fleet/snapshots/test_init.ambr | 4 - .../teslemetry/snapshots/test_init.ambr | 4 - .../components/tile/snapshots/test_init.ambr | 1 - .../tplink/snapshots/test_binary_sensor.ambr | 1 - .../tplink/snapshots/test_button.ambr | 1 - .../tplink/snapshots/test_camera.ambr | 1 - .../tplink/snapshots/test_climate.ambr | 1 - .../components/tplink/snapshots/test_fan.ambr | 1 - .../tplink/snapshots/test_number.ambr | 1 - .../tplink/snapshots/test_select.ambr | 1 - .../tplink/snapshots/test_sensor.ambr | 1 - .../tplink/snapshots/test_siren.ambr | 1 - .../tplink/snapshots/test_switch.ambr | 1 - .../tplink/snapshots/test_vacuum.ambr | 1 - .../components/tuya/snapshots/test_init.ambr | 1 - .../twentemilieu/snapshots/test_calendar.ambr | 1 - .../twentemilieu/snapshots/test_sensor.ambr | 5 - .../uptime/snapshots/test_sensor.ambr | 1 - .../velbus/snapshots/test_init.ambr | 10 - .../components/vesync/snapshots/test_fan.ambr | 12 - .../vesync/snapshots/test_light.ambr | 12 - .../vesync/snapshots/test_sensor.ambr | 12 - .../vesync/snapshots/test_switch.ambr | 12 - .../webostv/snapshots/test_media_player.ambr | 1 - .../whois/snapshots/test_sensor.ambr | 10 - .../withings/snapshots/test_init.ambr | 2 - .../wled/snapshots/test_button.ambr | 1 - .../wled/snapshots/test_number.ambr | 2 - .../wled/snapshots/test_select.ambr | 4 - .../wled/snapshots/test_switch.ambr | 4 - .../wmspro/snapshots/test_cover.ambr | 1 - .../wmspro/snapshots/test_init.ambr | 12 - .../wmspro/snapshots/test_light.ambr | 1 - .../wmspro/snapshots/test_scene.ambr | 1 - .../wolflink/snapshots/test_sensor.ambr | 1 - tests/components/wyoming/test_devices.py | 5 +- .../yale/snapshots/test_binary_sensor.ambr | 1 - .../components/yale/snapshots/test_lock.ambr | 1 - .../snapshots/test_entity_platform.ambr | 2 - tests/helpers/test_device_registry.py | 48 +++- tests/syrupy.py | 2 + 128 files changed, 180 insertions(+), 795 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 8a2a182c79696..0d0f5183566af 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -430,7 +430,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: "model": device.model, "sw_version": device.sw_version, "hw_version": device.hw_version, - "has_suggested_area": device.suggested_area is not None, "has_configuration_url": device.configuration_url is not None, "via_device": None, } diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index a1a9d4ed6b4a3..6487830675fc6 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -116,6 +116,8 @@ async def async_get_config_entry_diagnostics( entities.append({"entity": entity_dict, "state": state_dict}) device_dict = asdict(device) device_dict.pop("_cache", None) + # This can be removed when suggested_area is removed from DeviceEntry + device_dict.pop("_suggested_area") device_entities.append({"device": device_dict, "entities": entities}) # remove envoy serial diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c8b4428a7cc2c..d3866d8c9c340 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -32,6 +32,7 @@ from . import storage, translation from .debounce import Debouncer +from .deprecation import deprecated_function from .frame import ReportBehavior, report_usage from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment from .registry import BaseRegistry, BaseRegistryItems, RegistryIndexType @@ -67,6 +68,7 @@ ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 +# Can be removed when suggested_area is removed from DeviceEntry RUNTIME_ONLY_ATTRS = {"suggested_area"} CONFIGURATION_URL_SCHEMES = {"http", "https", "homeassistant"} @@ -343,7 +345,8 @@ class DeviceEntry: name: str | None = attr.ib(default=None) primary_config_entry: str | None = attr.ib(default=None) serial_number: str | None = attr.ib(default=None) - suggested_area: str | None = attr.ib(default=None) + # Suggested area is deprecated and will be removed from DeviceEntry in 2026.9. + _suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) via_device_id: str | None = attr.ib(default=None) # This value is not stored, just used to keep track of events to fire. @@ -442,6 +445,14 @@ def as_storage_fragment(self) -> json_fragment: ) ) + @property + @deprecated_function( + "code which ignores suggested_area", breaks_in_ha_version="2026.9" + ) + def suggested_area(self) -> str | None: + """Return the suggested area for this device entry.""" + return self._suggested_area + @attr.s(frozen=True, slots=True) class DeletedDeviceEntry: @@ -1197,6 +1208,7 @@ def async_update_device( # noqa: C901 ("name", name), ("name_by_user", name_by_user), ("serial_number", serial_number), + # Can be removed when suggested_area is removed from DeviceEntry ("suggested_area", suggested_area), ("sw_version", sw_version), ("via_device_id", via_device_id), @@ -1211,6 +1223,7 @@ def async_update_device( # noqa: C901 if not new_values: return old + # This condition can be removed when suggested_area is removed from DeviceEntry if not RUNTIME_ONLY_ATTRS.issuperset(new_values): # Change modified_at if we are changing something that we store new_values["modified_at"] = utcnow() @@ -1233,6 +1246,7 @@ def async_update_device( # noqa: C901 # firing events for data we have nothing to compare # against since its never saved on disk if RUNTIME_ONLY_ATTRS.issuperset(new_values): + # This can be removed when suggested_area is removed from DeviceEntry return new self.async_schedule_save() diff --git a/tests/components/acaia/snapshots/test_init.ambr b/tests/components/acaia/snapshots/test_init.ambr index c7a11cb58dfcb..d518de056b262 100644 --- a/tests/components/acaia/snapshots/test_init.ambr +++ b/tests/components/acaia/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Kitchen', 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index b3181fddfebc2..96ce43260aa83 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '84fce612f5b8', - 'suggested_area': None, 'sw_version': '3.1.1', 'via_device_id': None, }) @@ -68,7 +67,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '84fce612f5b8', - 'suggested_area': None, 'sw_version': '3.1.1', 'via_device_id': None, }) diff --git a/tests/components/alexa_devices/snapshots/test_init.ambr b/tests/components/alexa_devices/snapshots/test_init.ambr index e0460c4c1730f..c396c65246a30 100644 --- a/tests/components/alexa_devices/snapshots/test_init.ambr +++ b/tests/components/alexa_devices/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'echo_test_serial_number', - 'suggested_area': None, 'sw_version': 'echo_test_software_version', 'via_device_id': None, }) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 90f3049d8fdf8..0e14d55662018 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1051,7 +1051,6 @@ async def test_devices_payload( "hw_version": "test-hw-version", "integration": "hue", "is_custom_integration": False, - "has_suggested_area": True, "has_configuration_url": True, "via_device": None, }, @@ -1063,7 +1062,6 @@ async def test_devices_payload( "hw_version": None, "integration": "hue", "is_custom_integration": False, - "has_suggested_area": False, "has_configuration_url": False, "via_device": 0, }, diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index e647b7fa6a54e..f814106870bb3 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'serial', - 'suggested_area': 'Basement', 'sw_version': '2.14', 'via_device_id': None, }) diff --git a/tests/components/apcupsd/snapshots/test_init.ambr b/tests/components/apcupsd/snapshots/test_init.ambr index 39f28b528fcc9..6ca412f7e340f 100644 --- a/tests/components/apcupsd/snapshots/test_init.ambr +++ b/tests/components/apcupsd/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.14.14 (31 May 2016) unknown', 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/august/snapshots/test_binary_sensor.ambr b/tests/components/august/snapshots/test_binary_sensor.ambr index be5947372f5a2..c9a7b7ba039c9 100644 --- a/tests/components/august/snapshots/test_binary_sensor.ambr +++ b/tests/components/august/snapshots/test_binary_sensor.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'tmt100 Name', 'sw_version': '3.1.0-HYDRC75+201909251139', 'via_device_id': None, }) diff --git a/tests/components/august/snapshots/test_lock.ambr b/tests/components/august/snapshots/test_lock.ambr index 0a594fed1ee4e..eb2cf7a815a74 100644 --- a/tests/components/august/snapshots/test_lock.ambr +++ b/tests/components/august/snapshots/test_lock.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'online_with_doorsense Name', 'sw_version': 'undefined-4.3.0-1.8.14', 'via_device_id': None, }) diff --git a/tests/components/axis/snapshots/test_hub.ambr b/tests/components/axis/snapshots/test_hub.ambr index 9e407bfef0b7e..ab4745011ddfb 100644 --- a/tests/components/axis/snapshots/test_hub.ambr +++ b/tests/components/axis/snapshots/test_hub.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '00:40:8c:12:34:56', - 'suggested_area': None, 'sw_version': '9.10.1', 'via_device_id': None, }) @@ -68,7 +67,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '00:40:8c:12:34:56', - 'suggested_area': None, 'sw_version': '9.80.1', 'via_device_id': None, }) diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 0aaff0edfe71d..c8ced85c93350 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -11,7 +11,11 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ASSUMED_STATE, CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from .common import ( @@ -202,7 +206,9 @@ async def test_old_identifiers_are_removed( async def test_smart_by_bond_device_suggested_area( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test we can setup a smart by bond device and get the suggested area.""" config_entry = MockConfigEntry( @@ -241,11 +247,13 @@ async def test_smart_by_bond_device_suggested_area( device = device_registry.async_get_device(identifiers={(DOMAIN, "KXXX12345")}) assert device is not None - assert device.suggested_area == "Den" + assert device.area_id == area_registry.async_get_area_by_name("Den").id async def test_bridge_device_suggested_area( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test we can setup a bridge bond device and get the suggested area.""" config_entry = MockConfigEntry( @@ -289,7 +297,7 @@ async def test_bridge_device_suggested_area( device = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert device is not None - assert device.suggested_area == "Office" + assert device.area_id == area_registry.async_get_area_by_name("Office").id async def test_device_remove_devices( diff --git a/tests/components/cambridge_audio/snapshots/test_init.ambr b/tests/components/cambridge_audio/snapshots/test_init.ambr index 7f4bbed36f728..71a54cdb00103 100644 --- a/tests/components/cambridge_audio/snapshots/test_init.ambr +++ b/tests/components/cambridge_audio/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0020c2d8', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/deconz/snapshots/test_hub.ambr b/tests/components/deconz/snapshots/test_hub.ambr index 06067b69c1701..b171dafbd5de7 100644 --- a/tests/components/deconz/snapshots/test_hub.ambr +++ b/tests/components/deconz/snapshots/test_hub.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/devolo_home_network/snapshots/test_init.ambr b/tests/components/devolo_home_network/snapshots/test_init.ambr index 27ffd981b1e9f..4f965ce8d057b 100644 --- a/tests/components/devolo_home_network/snapshots/test_init.ambr +++ b/tests/components/devolo_home_network/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) @@ -68,7 +67,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) @@ -101,7 +99,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index e403c93739436..642f0db68131f 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'E1234567890000000001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index 2f1c2107b523c..5ff3710dfd772 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -80,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -166,7 +165,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index 16f202240793c..8ee893f6be5df 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -112,7 +112,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) @@ -232,7 +231,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) @@ -352,7 +350,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index 3592e88f9759d..ebf98ff02ae84 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -87,7 +87,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -183,7 +182,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -279,7 +277,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -372,7 +369,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -468,7 +464,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index f29c16d0cae01..8c75ed137b1bc 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -79,7 +79,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -164,7 +163,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 3a7f4e4fb9fa4..be638168b3483 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -60,7 +60,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -308,7 +307,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -939,7 +937,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -1187,7 +1184,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -1862,7 +1858,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -2110,7 +2105,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -2806,7 +2800,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -3356,7 +3349,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 8d2dd211869a7..fec957a95605c 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -50,6 +50,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( + area_registry as ar, device_registry as dr, entity_registry as er, issue_registry as ir, @@ -1170,6 +1171,7 @@ async def test_esphome_user_services_changes( async def test_esphome_device_with_suggested_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, @@ -1184,11 +1186,12 @@ async def test_esphome_device_with_suggested_area( dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} ) - assert dev.suggested_area == "kitchen" + assert dev.area_id == area_registry.async_get_area_by_name("kitchen").id async def test_esphome_device_area_priority( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, @@ -1207,7 +1210,7 @@ async def test_esphome_device_area_priority( connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} ) # Should use device_info.area.name instead of suggested_area - assert dev.suggested_area == "Living Room" + assert dev.area_id == area_registry.async_get_area_by_name("Living Room").id async def test_esphome_device_with_project( @@ -1535,6 +1538,7 @@ async def test_assist_in_progress_issue_deleted( async def test_sub_device_creation( hass: HomeAssistant, + area_registry: ar.AreaRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, ) -> None: @@ -1571,7 +1575,7 @@ async def test_sub_device_creation( connections={(dr.CONNECTION_NETWORK_MAC, device.device_info.mac_address)} ) assert main_device is not None - assert main_device.suggested_area == "Main Hub" + assert main_device.area_id == area_registry.async_get_area_by_name("Main Hub").id # Check sub devices are created sub_device_1 = device_registry.async_get_device( @@ -1579,7 +1583,9 @@ async def test_sub_device_creation( ) assert sub_device_1 is not None assert sub_device_1.name == "Motion Sensor" - assert sub_device_1.suggested_area == "Living Room" + assert ( + sub_device_1.area_id == area_registry.async_get_area_by_name("Living Room").id + ) assert sub_device_1.via_device_id == main_device.id sub_device_2 = device_registry.async_get_device( @@ -1587,7 +1593,9 @@ async def test_sub_device_creation( ) assert sub_device_2 is not None assert sub_device_2.name == "Light Switch" - assert sub_device_2.suggested_area == "Living Room" + assert ( + sub_device_2.area_id == area_registry.async_get_area_by_name("Living Room").id + ) assert sub_device_2.via_device_id == main_device.id sub_device_3 = device_registry.async_get_device( @@ -1595,7 +1603,7 @@ async def test_sub_device_creation( ) assert sub_device_3 is not None assert sub_device_3.name == "Temperature Sensor" - assert sub_device_3.suggested_area == "Bedroom" + assert sub_device_3.area_id == area_registry.async_get_area_by_name("Bedroom").id assert sub_device_3.via_device_id == main_device.id @@ -1731,6 +1739,7 @@ async def test_sub_device_with_empty_name( async def test_sub_device_references_main_device_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, ) -> None: @@ -1772,28 +1781,34 @@ async def test_sub_device_references_main_device_area( connections={(dr.CONNECTION_NETWORK_MAC, device.device_info.mac_address)} ) assert main_device is not None - assert main_device.suggested_area == "Main Hub Area" + assert ( + main_device.area_id == area_registry.async_get_area_by_name("Main Hub Area").id + ) # Check sub device 1 uses main device's area sub_device_1 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_11111111")} ) assert sub_device_1 is not None - assert sub_device_1.suggested_area == "Main Hub Area" + assert ( + sub_device_1.area_id == area_registry.async_get_area_by_name("Main Hub Area").id + ) # Check sub device 2 uses Living Room sub_device_2 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_22222222")} ) assert sub_device_2 is not None - assert sub_device_2.suggested_area == "Living Room" + assert ( + sub_device_2.area_id == area_registry.async_get_area_by_name("Living Room").id + ) # Check sub device 3 uses Bedroom sub_device_3 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_33333333")} ) assert sub_device_3 is not None - assert sub_device_3.suggested_area == "Bedroom" + assert sub_device_3.area_id == area_registry.async_get_area_by_name("Bedroom").id @patch("homeassistant.components.esphome.manager.secrets.token_bytes") diff --git a/tests/components/flo/snapshots/test_init.ambr b/tests/components/flo/snapshots/test_init.ambr index edba0ebe162a6..51e7bbd6dce4c 100644 --- a/tests/components/flo/snapshots/test_init.ambr +++ b/tests/components/flo/snapshots/test_init.ambr @@ -32,7 +32,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '6.1.1', 'via_device_id': None, }), @@ -67,7 +66,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111112', - 'suggested_area': None, 'sw_version': '1.1.15', 'via_device_id': None, }), diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr index d2af92b3f8f04..c26d39a5e253d 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_init.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.2.3', 'via_device_id': None, }) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index 0c57935589ba3..f11791b8ed109 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -59,7 +58,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -90,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -121,7 +118,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/homee/snapshots/test_init.ambr b/tests/components/homee/snapshots/test_init.ambr index 664740dbeac6d..dc56290e93e5d 100644 --- a/tests/components/homee/snapshots/test_init.ambr +++ b/tests/components/homee/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.2.3', 'via_device_id': None, }) @@ -64,7 +63,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.54', 'via_device_id': , }) diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 4540cfd239a3d..556be38f702f3 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -34,7 +34,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234', - 'suggested_area': None, 'sw_version': '0.8.16', }), 'entities': list([ @@ -665,7 +664,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000A', - 'suggested_area': None, 'sw_version': '2.1.6', }), 'entities': list([ @@ -747,7 +745,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000D', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -1005,7 +1002,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000B', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -1263,7 +1259,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000C', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -1525,7 +1520,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00aa00000a0', - 'suggested_area': None, 'sw_version': '3.3.0', }), 'entities': list([ @@ -1746,7 +1740,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '158d0007c59c6a', - 'suggested_area': None, 'sw_version': '0', }), 'entities': list([ @@ -1923,7 +1916,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0000000123456789', - 'suggested_area': None, 'sw_version': '1.4.7', }), 'entities': list([ @@ -2215,7 +2207,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '111a1111a1a111', - 'suggested_area': None, 'sw_version': '9', }), 'entities': list([ @@ -2349,7 +2340,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00A0000000000', - 'suggested_area': None, 'sw_version': '1.10.931', }), 'entities': list([ @@ -2863,7 +2853,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1020301376', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -3335,7 +3324,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -3510,7 +3498,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -3992,7 +3979,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4167,7 +4153,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4346,7 +4331,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4612,7 +4596,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4871,7 +4854,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5130,7 +5112,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5389,7 +5370,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5648,7 +5628,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5914,7 +5893,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6173,7 +6151,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6432,7 +6409,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6698,7 +6674,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6957,7 +6932,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '4.8.70226', }), 'entities': list([ @@ -7357,7 +7331,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -7623,7 +7596,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -7886,7 +7858,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -8372,7 +8343,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -8497,7 +8467,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -8798,7 +8767,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -8973,7 +8941,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -9152,7 +9119,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789016', - 'suggested_area': None, 'sw_version': '4.7.340214', }), 'entities': list([ @@ -9647,7 +9613,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '4.5.130201', }), 'entities': list([ @@ -9958,7 +9923,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', - 'suggested_area': None, 'sw_version': '1.2.8', }), 'entities': list([ @@ -10341,7 +10305,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', - 'suggested_area': None, 'sw_version': '1.2.9', }), 'entities': list([ @@ -10712,7 +10675,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-1', - 'suggested_area': None, 'sw_version': '5.0.18', }), 'entities': list([ @@ -10934,7 +10896,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-2', - 'suggested_area': None, 'sw_version': '5.0.18', }), 'entities': list([ @@ -11062,7 +11023,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -11236,7 +11196,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -11318,7 +11277,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -11496,7 +11454,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11629,7 +11586,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11711,7 +11667,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11849,7 +11804,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12197,7 +12151,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12283,7 +12236,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12365,7 +12317,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', - 'suggested_area': None, 'sw_version': '1.4.84', }), 'entities': list([ @@ -12551,7 +12502,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -12725,7 +12675,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12807,7 +12756,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -12985,7 +12933,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13118,7 +13065,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13200,7 +13146,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13339,7 +13284,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13421,7 +13365,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13560,7 +13503,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -13917,7 +13859,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14003,7 +13944,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14085,7 +14025,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14278,7 +14217,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14360,7 +14298,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14553,7 +14490,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14635,7 +14571,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', - 'suggested_area': None, 'sw_version': '1.4.84', }), 'entities': list([ @@ -14836,7 +14771,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00000001', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -15050,7 +14984,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462395276914', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15197,7 +15130,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462395276939', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15344,7 +15276,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462403113447', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15491,7 +15422,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462403233419', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15638,7 +15568,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462412411853', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15795,7 +15724,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462412413293', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15952,7 +15880,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462389072572', - 'suggested_area': None, 'sw_version': '45.1.17846', }), 'entities': list([ @@ -16286,7 +16213,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462378982941', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16420,7 +16346,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462378983942', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16554,7 +16479,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462379122122', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16688,7 +16612,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462379123707', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16822,7 +16745,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462383114163', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16956,7 +16878,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462383114193', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -17090,7 +17011,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462385996792', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -17224,7 +17144,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456', - 'suggested_area': None, 'sw_version': '1.32.1932126170', }), 'entities': list([ @@ -17310,7 +17229,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', - 'suggested_area': None, 'sw_version': '2.2.15', }), 'entities': list([ @@ -17463,7 +17381,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'EUCP03190xxxxx48', - 'suggested_area': None, 'sw_version': '2.3.7', }), 'entities': list([ @@ -17642,7 +17559,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'CNNT061751001372', - 'suggested_area': None, 'sw_version': '1.0.3', }), 'entities': list([ @@ -17862,7 +17778,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'XXXXXXXX', - 'suggested_area': None, 'sw_version': '3.40.XX', }), 'entities': list([ @@ -18162,7 +18077,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '999AAAAAA999', - 'suggested_area': None, 'sw_version': '04.71.04', }), 'entities': list([ @@ -18354,7 +18268,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '39024290', - 'suggested_area': None, 'sw_version': '001.005', }), 'entities': list([ @@ -18487,7 +18400,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '12344331', - 'suggested_area': None, 'sw_version': '08.08', }), 'entities': list([ @@ -18573,7 +18485,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'HH41234', - 'suggested_area': None, 'sw_version': '4.2.3', }), 'entities': list([ @@ -18869,7 +18780,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'BB1121', - 'suggested_area': None, 'sw_version': '4.1.9', }), 'entities': list([ @@ -19007,7 +18917,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', - 'suggested_area': None, 'sw_version': '2.8.1', }), 'entities': list([ @@ -19357,7 +19266,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', - 'suggested_area': None, 'sw_version': '1.4.40', }), 'entities': list([ @@ -19642,7 +19550,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'g738658', - 'suggested_area': None, 'sw_version': '80.0.0', }), 'entities': list([ @@ -19953,7 +19860,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234', - 'suggested_area': None, 'sw_version': '1.0.3', }), 'entities': list([ @@ -20125,7 +20031,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAAAAAAAA', - 'suggested_area': None, 'sw_version': '59', }), 'entities': list([ @@ -20451,7 +20356,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00aa0000aa0a', - 'suggested_area': None, 'sw_version': '1.0.4', }), 'entities': list([ @@ -20897,7 +20801,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21071,7 +20974,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0101.3521.0436', - 'suggested_area': None, 'sw_version': '1.3.0', }), 'entities': list([ @@ -21153,7 +21055,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '', - 'suggested_area': None, 'sw_version': '', }), 'entities': list([ @@ -21331,7 +21232,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21505,7 +21405,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21679,7 +21578,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21853,7 +21751,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0401.3521.0679', - 'suggested_area': None, 'sw_version': '1.3.0', }), 'entities': list([ @@ -21935,7 +21832,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -22113,7 +22009,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', - 'suggested_area': None, 'sw_version': '004.027.000', }), 'entities': list([ @@ -22242,7 +22137,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234567890abcd', - 'suggested_area': None, 'sw_version': '', }), 'entities': list([ @@ -22432,7 +22326,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '0.0.0', }), 'entities': list([ @@ -22563,7 +22456,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '3.3.0', }), 'entities': list([ @@ -22978,7 +22870,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '16.0.0', }), 'entities': list([ @@ -23208,7 +23099,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'a1a11a1', - 'suggested_area': None, 'sw_version': '70', }), 'entities': list([ @@ -23290,7 +23180,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'a11b111', - 'suggested_area': None, 'sw_version': '16', }), 'entities': list([ @@ -23516,7 +23405,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1111111a114a111a', - 'suggested_area': None, 'sw_version': '48', }), 'entities': list([ @@ -23647,7 +23535,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '0.0.0', }), 'entities': list([ @@ -23778,7 +23665,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '15.0.0', }), 'entities': list([ @@ -23908,7 +23794,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AM01121849000327', - 'suggested_area': None, 'sw_version': '3.121.2', }), 'entities': list([ @@ -24229,7 +24114,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'EU0121203xxxxx07', - 'suggested_area': None, 'sw_version': '1.101.2', }), 'entities': list([ diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 656978a08a2f1..166fd1a9e652a 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -328,6 +328,8 @@ async def test_snapshots( device_dict.pop("created_at", None) device_dict.pop("modified_at", None) device_dict.pop("_cache", None) + # This can be removed when suggested_area is removed from DeviceEntry + device_dict.pop("_suggested_area") devices.append({"device": device_dict, "entities": entities}) diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index a07c0745c45ab..3b6264367e2f3 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -80,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index 3224a0cc63e92..b75b89269f1f1 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -89,7 +89,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -184,7 +183,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_select.ambr b/tests/components/homewizard/snapshots/test_select.ambr index ecfd80e04dabe..dd331c3f49b93 100644 --- a/tests/components/homewizard/snapshots/test_select.ambr +++ b/tests/components/homewizard/snapshots/test_select.ambr @@ -90,7 +90,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 9f95e140edcbc..f870170bae9d3 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -119,7 +118,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -212,7 +210,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -305,7 +302,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -398,7 +394,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -491,7 +486,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -584,7 +578,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -677,7 +670,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -763,7 +755,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -856,7 +847,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -945,7 +935,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -1030,7 +1019,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1123,7 +1111,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1216,7 +1203,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1309,7 +1295,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1402,7 +1387,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1495,7 +1479,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1588,7 +1571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1678,7 +1660,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1771,7 +1752,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1864,7 +1844,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1949,7 +1928,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2038,7 +2016,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2131,7 +2108,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2224,7 +2200,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2317,7 +2292,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2410,7 +2384,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2503,7 +2476,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2596,7 +2568,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2689,7 +2660,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2782,7 +2752,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2875,7 +2844,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2968,7 +2936,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3061,7 +3028,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3154,7 +3120,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3244,7 +3209,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3334,7 +3298,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3424,7 +3387,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3517,7 +3479,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3610,7 +3571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3703,7 +3663,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3796,7 +3755,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3889,7 +3847,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3982,7 +3939,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4075,7 +4031,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4168,7 +4123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4261,7 +4215,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4354,7 +4307,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4439,7 +4391,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4528,7 +4479,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4618,7 +4568,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4711,7 +4660,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4804,7 +4752,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4897,7 +4844,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4982,7 +4928,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5075,7 +5020,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5168,7 +5112,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5261,7 +5204,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5354,7 +5296,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5447,7 +5388,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5540,7 +5480,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5633,7 +5572,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5726,7 +5664,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5819,7 +5756,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5912,7 +5848,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6005,7 +5940,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6090,7 +6024,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6180,7 +6113,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6273,7 +6205,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6358,7 +6289,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6451,7 +6381,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6544,7 +6473,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6637,7 +6565,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6722,7 +6649,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6807,7 +6733,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6906,7 +6831,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6999,7 +6923,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7092,7 +7015,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7185,7 +7107,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7278,7 +7199,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7363,7 +7283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7448,7 +7367,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7533,7 +7451,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7618,7 +7535,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7703,7 +7619,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7788,7 +7703,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7877,7 +7791,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7962,7 +7875,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8047,7 +7959,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'gas_meter_G001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8136,7 +8047,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'heat_meter_H001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8225,7 +8135,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_IH001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8310,7 +8219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'warm_water_meter_WW001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8399,7 +8307,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'water_meter_W001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8492,7 +8399,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8582,7 +8488,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8675,7 +8580,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8768,7 +8672,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8861,7 +8764,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8946,7 +8848,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9039,7 +8940,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9132,7 +9032,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9225,7 +9124,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9318,7 +9216,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9411,7 +9308,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9504,7 +9400,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9597,7 +9492,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9690,7 +9584,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9783,7 +9676,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9876,7 +9768,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9969,7 +9860,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10054,7 +9944,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10144,7 +10033,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10237,7 +10125,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10322,7 +10209,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10415,7 +10301,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10508,7 +10393,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10601,7 +10485,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10686,7 +10569,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10771,7 +10653,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10870,7 +10751,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10963,7 +10843,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11056,7 +10935,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11149,7 +11027,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11242,7 +11119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11327,7 +11203,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11412,7 +11287,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11497,7 +11371,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11582,7 +11455,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11667,7 +11539,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11752,7 +11623,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11841,7 +11711,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11926,7 +11795,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12011,7 +11879,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12100,7 +11967,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12189,7 +12055,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12274,7 +12139,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12363,7 +12227,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12456,7 +12319,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12546,7 +12408,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12639,7 +12500,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12732,7 +12592,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12825,7 +12684,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12918,7 +12776,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13011,7 +12868,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13104,7 +12960,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13197,7 +13052,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13290,7 +13144,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13383,7 +13236,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13476,7 +13328,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13569,7 +13420,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13662,7 +13512,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13755,7 +13604,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13848,7 +13696,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13933,7 +13780,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14026,7 +13872,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14111,7 +13956,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14204,7 +14048,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14297,7 +14140,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14390,7 +14232,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14483,7 +14324,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14576,7 +14416,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14669,7 +14508,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14762,7 +14600,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14847,7 +14684,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14932,7 +14768,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15017,7 +14852,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15102,7 +14936,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15187,7 +15020,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15272,7 +15104,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15361,7 +15192,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15446,7 +15276,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15535,7 +15364,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15628,7 +15456,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15721,7 +15548,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15814,7 +15640,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15907,7 +15732,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15992,7 +15816,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -16081,7 +15904,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16174,7 +15996,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16267,7 +16088,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16360,7 +16180,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16453,7 +16272,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16546,7 +16364,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16639,7 +16456,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16729,7 +16545,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16822,7 +16637,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16915,7 +16729,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -17008,7 +16821,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -17093,7 +16905,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -17182,7 +16993,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17275,7 +17085,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17364,7 +17173,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17449,7 +17257,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17538,7 +17345,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17631,7 +17437,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17724,7 +17529,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17817,7 +17621,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17910,7 +17713,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18003,7 +17805,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18096,7 +17897,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18186,7 +17986,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18279,7 +18078,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18372,7 +18170,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18457,7 +18254,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18546,7 +18342,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18639,7 +18434,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18732,7 +18526,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18825,7 +18618,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18918,7 +18710,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19011,7 +18802,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19104,7 +18894,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19197,7 +18986,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19290,7 +19078,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19383,7 +19170,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19476,7 +19262,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19569,7 +19354,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19662,7 +19446,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19752,7 +19535,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19842,7 +19624,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19932,7 +19713,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20025,7 +19805,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20118,7 +19897,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20211,7 +19989,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20304,7 +20081,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20397,7 +20173,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20490,7 +20265,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20583,7 +20357,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20676,7 +20449,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20769,7 +20541,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20862,7 +20633,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20947,7 +20717,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index c4e67003b58b4..49916a59d9edd 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -79,7 +79,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -164,7 +163,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -250,7 +248,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -335,7 +332,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -420,7 +416,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -506,7 +501,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -591,7 +585,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -676,7 +669,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -761,7 +753,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -846,7 +837,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -931,7 +921,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 807996f10937f..5f287b1d8e3dd 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -11,7 +11,11 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.util import color as color_util from .conftest import create_config_entry @@ -776,6 +780,7 @@ def test_hs_color() -> None: async def test_group_features( hass: HomeAssistant, + area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, mock_bridge_v1: Mock, @@ -966,16 +971,22 @@ async def test_group_features( entry = entity_registry.async_get("light.hue_lamp_1") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area is None + assert device_entry.area_id is None entry = entity_registry.async_get("light.hue_lamp_2") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Living Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living Room").id + ) entry = entity_registry.async_get("light.hue_lamp_3") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Living Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living Room").id + ) entry = entity_registry.async_get("light.hue_lamp_4") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Dining Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Dining Room").id + ) diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index 1428a75d7b428..e0627ad9da8ed 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': 'Garden', 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr index b7aa14ef0bfbf..e2b8eeba8117c 100644 --- a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/iotty/snapshots/test_switch.ambr b/tests/components/iotty/snapshots/test_switch.ambr index 058a5d35cd018..04712dbf022a6 100644 --- a/tests/components/iotty/snapshots/test_switch.ambr +++ b/tests/components/iotty/snapshots/test_switch.ambr @@ -39,7 +39,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index 7329eec7f7076..6a5f5371a9d0a 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ituran/snapshots/test_init.ambr b/tests/components/ituran/snapshots/test_init.ambr index b97aef6027be9..456687407e2c7 100644 --- a/tests/components/ituran/snapshots/test_init.ambr +++ b/tests/components/ituran/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '12345678', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 9c9f31a25445b..350ac16993854 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -75,7 +75,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -108,7 +107,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -189,7 +187,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -222,7 +219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/lamarzocco/snapshots/test_init.ambr b/tests/components/lamarzocco/snapshots/test_init.ambr index 18b2fd0fbc3c8..f11057f8620a2 100644 --- a/tests/components/lamarzocco/snapshots/test_init.ambr +++ b/tests/components/lamarzocco/snapshots/test_init.ambr @@ -35,7 +35,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GS012345', - 'suggested_area': None, 'sw_version': 'v1.17', 'via_device_id': None, }) diff --git a/tests/components/lektrico/snapshots/test_init.ambr b/tests/components/lektrico/snapshots/test_init.ambr index 35183bf5d75cb..a935f5cfa14f7 100644 --- a/tests/components/lektrico/snapshots/test_init.ambr +++ b/tests/components/lektrico/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '500006', - 'suggested_area': None, 'sw_version': '1.44', 'via_device_id': None, }) diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index e2a35bcb1b1dc..1b09d742876f9 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -14,7 +14,11 @@ from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.zeroconf import ( ATTR_PROPERTIES_ID, @@ -585,6 +589,7 @@ async def test_refuse_relays(hass: HomeAssistant) -> None: async def test_suggested_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -624,4 +629,4 @@ def __call__(self, callb=None, *args, **kwargs): entity = entity_registry.async_get(entity_id) device = device_registry.async_get(entity.device_id) - assert device.suggested_area == "My LIFX Group" + assert device.area_id == area_registry.async_get_area_by_name("My LIFX Group").id diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr index 46fb4c1d4e00a..4d3e9d7aeab50 100644 --- a/tests/components/mastodon/snapshots/test_init.ambr +++ b/tests/components/mastodon/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.4.0-nightly.2025-02-07', 'via_device_id': None, }) diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index aada173ffc365..e3a9e6089110f 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'v1.10.2', 'via_device_id': None, }) diff --git a/tests/components/meater/snapshots/test_init.ambr b/tests/components/meater/snapshots/test_init.ambr index 68e4ba32a4a37..95335942de6c6 100644 --- a/tests/components/meater/snapshots/test_init.ambr +++ b/tests/components/meater/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/miele/snapshots/test_init.ambr b/tests/components/miele/snapshots/test_init.ambr index eee976ab09f8c..9feeeb6523bfc 100644 --- a/tests/components/miele/snapshots/test_init.ambr +++ b/tests/components/miele/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'Dummy_Appliance_1', - 'suggested_area': None, 'sw_version': '31.17', 'via_device_id': None, }) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 15e203eab0624..fdaed0c323f47 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -32,7 +32,11 @@ ) from homeassistant.core import HassJobType, HomeAssistant from homeassistant.generated.mqtt import MQTT -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util @@ -1415,13 +1419,14 @@ async def help_test_entity_device_info_with_identifier( config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" - registry = dr.async_get(hass) + area_registry = ar.async_get(hass) + device_registry = dr.async_get(hass) data = json.dumps(config) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) + device = device_registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -1430,7 +1435,7 @@ async def help_test_entity_device_info_with_identifier( assert device.model_id == "XYZ001" assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" - assert device.suggested_area == "default_area" + assert device.area_id == area_registry.async_get_area_by_name("default_area").id assert device.configuration_url == "http://example.com" @@ -1450,13 +1455,14 @@ async def help_test_entity_device_info_with_connection( config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) config["unique_id"] = "veryunique" - registry = dr.async_get(hass) + area_registry = ar.async_get(hass) + device_registry = dr.async_get(hass) data = json.dumps(config) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device( + device = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} ) assert device is not None @@ -1467,7 +1473,7 @@ async def help_test_entity_device_info_with_connection( assert device.model_id == "XYZ001" assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" - assert device.suggested_area == "default_area" + assert device.area_id == area_registry.async_get_area_by_name("default_area").id assert device.configuration_url == "http://example.com" diff --git a/tests/components/myuplink/snapshots/test_init.ambr b/tests/components/myuplink/snapshots/test_init.ambr index 14be11c36ece2..56fb26b408426 100644 --- a/tests/components/myuplink/snapshots/test_init.ambr +++ b/tests/components/myuplink/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10001', - 'suggested_area': None, 'sw_version': '9682R7A', 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10002', - 'suggested_area': None, 'sw_version': '9682R7B', 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10003', - 'suggested_area': None, 'sw_version': '9682R7C', 'via_device_id': None, }) diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index 35e7f7efc2962..95fb1f9ed458e 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Corridor', 'sw_version': None, 'via_device_id': None, }) @@ -159,7 +155,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -192,7 +187,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -225,7 +219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -258,7 +251,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -291,7 +283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -324,7 +315,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -357,7 +347,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -390,7 +379,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -423,7 +411,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -456,7 +443,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -489,7 +475,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -522,7 +507,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -555,7 +539,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -588,7 +571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -621,7 +603,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -654,7 +635,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -687,7 +667,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -720,7 +699,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -753,7 +731,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -786,7 +763,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -819,7 +795,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -852,7 +827,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -885,7 +859,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -918,7 +891,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -951,7 +923,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -984,7 +955,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1017,7 +987,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1050,7 +1019,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Bureau', 'sw_version': None, 'via_device_id': None, }) @@ -1083,7 +1051,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Livingroom', 'sw_version': None, 'via_device_id': None, }) @@ -1116,7 +1083,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Entrada', 'sw_version': None, 'via_device_id': None, }) @@ -1149,7 +1115,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Cocina', 'sw_version': None, 'via_device_id': None, }) @@ -1182,7 +1147,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1215,7 +1179,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1248,7 +1211,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1281,7 +1243,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index 2a806be8ae176..2980e3f35f058 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'FFFFFFFFFFFFF', - 'suggested_area': None, 'sw_version': 'EC25AFFDR07A09M4G', 'via_device_id': None, }) diff --git a/tests/components/nut/test_init.py b/tests/components/nut/test_init.py index 6f1fb94478d97..18c038c17a002 100644 --- a/tests/components/nut/test_init.py +++ b/tests/components/nut/test_init.py @@ -17,7 +17,7 @@ STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import area_registry as ar, device_registry as dr from homeassistant.setup import async_setup_component from .util import _get_mock_nutclient, async_init_integration @@ -247,6 +247,7 @@ async def test_serial_number( async def test_device_location( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test for suggested location on device.""" @@ -269,7 +270,10 @@ async def test_device_location( ) assert device_entry is not None - assert device_entry.suggested_area == mock_device_location + assert ( + device_entry.area_id + == area_registry.async_get_area_by_name(mock_device_location).id + ) async def test_update_options(hass: HomeAssistant) -> None: diff --git a/tests/components/nyt_games/snapshots/test_init.ambr b/tests/components/nyt_games/snapshots/test_init.ambr index d9ce6f15a4d36..5ca9a2d8df237 100644 --- a/tests/components/nyt_games/snapshots/test_init.ambr +++ b/tests/components/nyt_games/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ohme/snapshots/test_init.ambr b/tests/components/ohme/snapshots/test_init.ambr index ccf09f546cf67..2e8304489d9ae 100644 --- a/tests/components/ohme/snapshots/test_init.ambr +++ b/tests/components/ohme/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'chargerid', - 'suggested_area': None, 'sw_version': 'v2.65', 'via_device_id': None, }) diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index 07e56a78fae7b..787551ad90e29 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.7.1-stable', 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.7.1-stable', 'via_device_id': None, }) diff --git a/tests/components/onedrive/snapshots/test_init.ambr b/tests/components/onedrive/snapshots/test_init.ambr index 9b2ed7e4d94e0..2f9cfc1a03801 100644 --- a/tests/components/onedrive/snapshots/test_init.ambr +++ b/tests/components/onedrive/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/onewire/snapshots/test_init.ambr b/tests/components/onewire/snapshots/test_init.ambr index 9b2a0e00a6231..26ed15fc8978f 100644 --- a/tests/components/onewire/snapshots/test_init.ambr +++ b/tests/components/onewire/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': , }) @@ -159,7 +155,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -192,7 +187,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -225,7 +219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -258,7 +251,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -291,7 +283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -324,7 +315,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222222', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -357,7 +347,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222223', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -390,7 +379,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -423,7 +411,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -456,7 +443,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -489,7 +475,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -522,7 +507,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -555,7 +539,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -588,7 +571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222222', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -621,7 +603,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -654,7 +635,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -687,7 +667,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111112', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -720,7 +699,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111113', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) diff --git a/tests/components/openai_conversation/snapshots/test_init.ambr b/tests/components/openai_conversation/snapshots/test_init.ambr index 4eff869b0160f..0058416b2547c 100644 --- a/tests/components/openai_conversation/snapshots/test_init.ambr +++ b/tests/components/openai_conversation/snapshots/test_init.ambr @@ -21,7 +21,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -48,7 +47,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/overseerr/snapshots/test_init.ambr b/tests/components/overseerr/snapshots/test_init.ambr index 2709f532ef661..71c1b9ffd3a1c 100644 --- a/tests/components/overseerr/snapshots/test_init.ambr +++ b/tests/components/overseerr/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/palazzetti/snapshots/test_init.ambr b/tests/components/palazzetti/snapshots/test_init.ambr index fc96cab4fad05..b69982d9c0891 100644 --- a/tests/components/palazzetti/snapshots/test_init.ambr +++ b/tests/components/palazzetti/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.0.0', 'via_device_id': None, }) diff --git a/tests/components/peblar/snapshots/test_init.ambr b/tests/components/peblar/snapshots/test_init.ambr index 8a7cefc523df5..97c0737e402e5 100644 --- a/tests/components/peblar/snapshots/test_init.ambr +++ b/tests/components/peblar/snapshots/test_init.ambr @@ -35,7 +35,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '23-45-A4O-MOF', - 'suggested_area': None, 'sw_version': '1.6.1+1+WL-1', 'via_device_id': None, }) diff --git a/tests/components/rainforest_raven/snapshots/test_init.ambr b/tests/components/rainforest_raven/snapshots/test_init.ambr index 8a143f9963f47..f34d33d6c245b 100644 --- a/tests/components/rainforest_raven/snapshots/test_init.ambr +++ b/tests/components/rainforest_raven/snapshots/test_init.ambr @@ -32,7 +32,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.0.0 (7400)', 'via_device_id': None, }), diff --git a/tests/components/renault/snapshots/test_init.ambr b/tests/components/renault/snapshots/test_init.ambr index 9a10083b227b8..defb0f249ff50 100644 --- a/tests/components/renault/snapshots/test_init.ambr +++ b/tests/components/renault/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -63,7 +62,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -98,7 +96,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -133,7 +130,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -168,7 +164,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index c3aec4f09680d..bc5022a7724e4 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -9,7 +9,11 @@ from homeassistant.components.roku.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from . import UPNP_SERIAL @@ -77,12 +81,13 @@ async def test_roku_binary_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_rokutv_binary_sensors( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -158,4 +163,6 @@ async def test_rokutv_binary_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 7586e85b715ba..2607c79086a1c 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -60,7 +60,11 @@ ) from homeassistant.core import HomeAssistant from homeassistant.core_config import async_process_ha_core_config -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -100,7 +104,7 @@ async def test_setup( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/roku3-idle.json"], indirect=True) @@ -118,6 +122,7 @@ async def test_idle_setup( @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_tv_setup( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -146,7 +151,9 @@ async def test_tv_setup( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) @pytest.mark.parametrize( diff --git a/tests/components/roku/test_sensor.py b/tests/components/roku/test_sensor.py index e65424e3e6661..72f57729cc4e1 100644 --- a/tests/components/roku/test_sensor.py +++ b/tests/components/roku/test_sensor.py @@ -12,7 +12,11 @@ EntityCategory, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from . import UPNP_SERIAL @@ -60,12 +64,13 @@ async def test_roku_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_rokutv_sensors( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -106,4 +111,6 @@ async def test_rokutv_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 8eb7700606117..3715f994fb060 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/russound_rio/snapshots/test_init.ambr b/tests/components/russound_rio/snapshots/test_init.ambr index e3185a06b2452..0fcebb8a6e566 100644 --- a/tests/components/russound_rio/snapshots/test_init.ambr +++ b/tests/components/russound_rio/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index b29b824a7ddac..f9006c7fd5274 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -32,7 +32,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -67,7 +66,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -106,7 +104,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/schlage/snapshots/test_init.ambr b/tests/components/schlage/snapshots/test_init.ambr index a7f94b80038e3..4e57ad5d5c657 100644 --- a/tests/components/schlage/snapshots/test_init.ambr +++ b/tests/components/schlage/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0', 'via_device_id': None, }) diff --git a/tests/components/sensibo/snapshots/test_entity.ambr b/tests/components/sensibo/snapshots/test_entity.ambr index 80ee847cb55c7..ee0b3835da49a 100644 --- a/tests/components/sensibo/snapshots/test_entity.ambr +++ b/tests/components/sensibo/snapshots/test_entity.ambr @@ -32,7 +32,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': 'Hallway', 'sw_version': 'SKY30046', 'via_device_id': None, }), @@ -67,7 +66,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0987654321', - 'suggested_area': 'Kitchen', 'sw_version': 'PUR00111', 'via_device_id': None, }), @@ -102,7 +100,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0987654329', - 'suggested_area': 'Bedroom', 'sw_version': 'PUR00111', 'via_device_id': None, }), @@ -133,7 +130,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'V17', 'via_device_id': , }), diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 0ee34eebf3f3c..f0193b6ce1c8a 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), @@ -161,7 +160,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 39dd9e512aeca..e3e5475ca34c0 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index cd762a4b2ea98..681c3a841914f 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/slide_local/snapshots/test_init.ambr b/tests/components/slide_local/snapshots/test_init.ambr index 5b1a9f5ce2f31..e2dec748e2a50 100644 --- a/tests/components/slide_local/snapshots/test_init.ambr +++ b/tests/components/slide_local/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890ab', - 'suggested_area': None, 'sw_version': 2, 'via_device_id': None, }) diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 6ce3992d2b49b..d63ac4e9ab4f9 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -30,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -63,7 +62,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Toilet', 'sw_version': None, 'via_device_id': None, }) @@ -96,7 +94,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -129,7 +126,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -162,7 +158,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -195,7 +190,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -228,7 +222,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -261,7 +254,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -294,7 +286,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -327,7 +318,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'ASM-KR-TP1-22-ACMB1M_16240426', 'via_device_id': None, }) @@ -360,7 +350,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AEH-WW-TP1-22-AE6000_17240903', 'via_device_id': None, }) @@ -393,7 +382,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -426,7 +414,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'ARTIK051_PRAC_20K_11230313', 'via_device_id': None, }) @@ -459,7 +446,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'ARA-WW-TP1-22-COMMON_11240702', 'via_device_id': None, }) @@ -492,7 +478,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -525,7 +510,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -558,7 +542,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'AKS-WW-TP2-20-MICROWAVE-OTR_40230125', 'via_device_id': None, }) @@ -591,7 +574,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AKS-WW-TP1X-21-OVEN_40211229', 'via_device_id': None, }) @@ -624,7 +606,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AKS-WW-TP1-20-OVEN-3-CR_40240205', 'via_device_id': None, }) @@ -657,7 +638,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'A-RFWW-TP2-21-COMMON_20220110', 'via_device_id': None, }) @@ -690,7 +670,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20240616.213423', 'via_device_id': None, }) @@ -723,7 +702,6 @@ '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, }) @@ -756,7 +734,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250123.105306', 'via_device_id': None, }) @@ -789,7 +766,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': '1.0', 'via_device_id': None, }) @@ -822,7 +798,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -855,7 +830,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -888,7 +862,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -921,7 +894,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_DW_A51_20_COMMON_30230714', 'via_device_id': None, }) @@ -954,7 +926,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_DF_TP2_20_COMMON_30230807', 'via_device_id': None, }) @@ -987,7 +958,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1020,7 +990,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1053,7 +1022,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_WM_TP2_20_COMMON_30230804', 'via_device_id': None, }) @@ -1086,7 +1054,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1119,7 +1086,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_TP1_21_COMMON_30240927', 'via_device_id': None, }) @@ -1152,7 +1118,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1185,7 +1150,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250206213001', 'via_device_id': None, }) @@ -1218,7 +1182,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250206151734', 'via_device_id': None, }) @@ -1251,7 +1214,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250308073247', 'via_device_id': None, }) @@ -1284,7 +1246,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1317,7 +1278,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1350,7 +1310,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1383,7 +1342,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1416,7 +1374,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1449,7 +1406,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1482,7 +1438,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1515,7 +1470,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.122.2', 'via_device_id': None, }) @@ -1548,7 +1502,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.122.2', 'via_device_id': None, }) @@ -1581,7 +1534,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'HW-Q80RWWB-1012.6', 'via_device_id': None, }) @@ -1614,7 +1566,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1647,7 +1598,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1680,7 +1630,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'V310XXU1AWK1', 'via_device_id': None, }) @@ -1713,7 +1662,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1746,7 +1694,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1779,7 +1726,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1812,7 +1758,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '6004971003', 'via_device_id': None, }) @@ -1845,7 +1790,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'SKY40147', 'via_device_id': None, }) @@ -1878,7 +1822,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1911,7 +1854,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1944,7 +1886,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.3.1 Build 240621 Rel.162048', 'via_device_id': None, }) @@ -1977,7 +1918,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'SAT-iMX8M23WWC-1010.5', 'via_device_id': None, }) @@ -2010,7 +1950,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'SAT-MT8532D24WWC-1016.0', 'via_device_id': None, }) @@ -2043,7 +1982,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'latest', 'via_device_id': None, }) @@ -2076,7 +2014,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'T-KTMAKUC-1290.3', 'via_device_id': None, }) @@ -2109,7 +2046,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2142,7 +2078,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2175,7 +2110,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2208,7 +2142,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2245,7 +2178,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': '000.055.00005', 'via_device_id': None, }) diff --git a/tests/components/smarty/snapshots/test_init.ambr b/tests/components/smarty/snapshots/test_init.ambr index a292cc97f47cb..ffa30051726d2 100644 --- a/tests/components/smarty/snapshots/test_init.ambr +++ b/tests/components/smarty/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 127, 'via_device_id': None, }) diff --git a/tests/components/smlight/snapshots/test_init.ambr b/tests/components/smlight/snapshots/test_init.ambr index ba37419925483..8f533a42e3682 100644 --- a/tests/components/smlight/snapshots/test_init.ambr +++ b/tests/components/smlight/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'core: v2.3.6 / zigbee: 20240314', 'via_device_id': None, }) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index b15d7698e05b3..84ad624cdc822 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -54,7 +54,7 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import area_registry as ar, entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, CONNECTION_UPNP, @@ -83,11 +83,15 @@ async def test_device_registry( assert reg_device.manufacturer == "Sonos" assert reg_device.name == "Zone A" # Default device provides battery info, area should not be suggested - assert reg_device.suggested_area is None + assert reg_device.area_id is None async def test_device_registry_not_portable( - hass: HomeAssistant, device_registry: DeviceRegistry, async_setup_sonos, soco + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: DeviceRegistry, + async_setup_sonos, + soco, ) -> None: """Test non-portable sonos device registered in the device registry to ensure area suggested.""" soco.get_battery_info.return_value = {} @@ -97,7 +101,7 @@ async def test_device_registry_not_portable( identifiers={("sonos", "RINCON_test")} ) assert reg_device is not None - assert reg_device.suggested_area == "Zone A" + assert reg_device.area_id == area_registry.async_get_area_by_name("Zone A").id async def test_entity_basic( diff --git a/tests/components/squeezebox/snapshots/test_init.ambr b/tests/components/squeezebox/snapshots/test_init.ambr index 3fc65be834a41..39664f9ecf278 100644 --- a/tests/components/squeezebox/snapshots/test_init.ambr +++ b/tests/components/squeezebox/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '', 'via_device_id': , }) @@ -72,7 +71,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '', 'via_device_id': , }) diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index 5d166018160ad..a5a591af94cbd 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -76,7 +76,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) @@ -158,7 +157,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 0e4bb4e4e411d..627f05432d224 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -80,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': None, }) diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index a1a98b028e3dc..54c648ba21ba4 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -77,7 +77,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) @@ -160,7 +159,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index ffa2c5df7fd93..acabe06142015 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -89,7 +89,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': None, }) diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index 28b5ef7a7ed78..cfaca7b81f38c 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0000-0000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index a568a7dcd8274..b7bf9e6bfa5d3 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -76,7 +76,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/tesla_fleet/snapshots/test_init.ambr b/tests/components/tesla_fleet/snapshots/test_init.ambr index c482d33de86aa..a669813a3a56c 100644 --- a/tests/components/tesla_fleet/snapshots/test_init.ambr +++ b/tests/components/tesla_fleet/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123456', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'LRWXF7EK4KC700000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '234', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index f1011034d63d1..39fc8d049847f 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123456', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'LRW3F7EK4NC700000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '234', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/tile/snapshots/test_init.ambr b/tests/components/tile/snapshots/test_init.ambr index ffdf6a6251a44..0c3e1faf09097 100644 --- a/tests/components/tile/snapshots/test_init.ambr +++ b/tests/components/tile/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '01.12.14.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index c8251bccd4f60..c12f73bd73780 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -430,7 +430,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr index 84cc8f73bf3ff..7d49d2aedbc0d 100644 --- a/tests/components/tplink/snapshots/test_button.ambr +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -612,7 +612,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_camera.ambr b/tests/components/tplink/snapshots/test_camera.ambr index f50c5d703624d..a0282401e5832 100644 --- a/tests/components/tplink/snapshots/test_camera.ambr +++ b/tests/components/tplink/snapshots/test_camera.ambr @@ -82,7 +82,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index df63291175a8b..4a38bdbbe5957 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -92,7 +92,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': , }) diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index ad0321accef4a..eb42e2a729882 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -196,7 +196,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index 5ff1d9c5458d7..bc71313bf9674 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr index 9fc5181c45d6b..6bcd24521e41e 100644 --- a/tests/components/tplink/snapshots/test_select.ambr +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 5c22c2f7d8322..f95390a8a57f8 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_siren.ambr b/tests/components/tplink/snapshots/test_siren.ambr index 761df4fcf2136..7f90915f62471 100644 --- a/tests/components/tplink/snapshots/test_siren.ambr +++ b/tests/components/tplink/snapshots/test_siren.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index 4b04587db0580..98584c7975936 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_vacuum.ambr b/tests/components/tplink/snapshots/test_vacuum.ambr index 68d14270b553e..e5b28f5ac7a79 100644 --- a/tests/components/tplink/snapshots/test_vacuum.ambr +++ b/tests/components/tplink/snapshots/test_vacuum.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 084e9a84401e5..fc30460bcc07b 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 915c0f5080e61..68f5a7b6adf73 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -107,7 +107,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index 9e8bb6f7381e0..ad435a833eeaf 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -76,7 +76,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -158,7 +157,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -240,7 +238,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -322,7 +319,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -404,7 +400,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index 5c9ed6d4683d0..cb4563e0fb57a 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -69,7 +69,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/velbus/snapshots/test_init.ambr b/tests/components/velbus/snapshots/test_init.ambr index 037ab7e62365d..29f92126f95a1 100644 --- a/tests/components/velbus/snapshots/test_init.ambr +++ b/tests/components/velbus/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }), @@ -59,7 +58,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }), @@ -90,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '2.0.0', 'via_device_id': None, }), @@ -121,7 +118,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6g7', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': , }), @@ -152,7 +148,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '12345', - 'suggested_area': None, 'sw_version': '1.0.1', 'via_device_id': , }), @@ -183,7 +178,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': , }), @@ -214,7 +208,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'asdfghjk', - 'suggested_area': None, 'sw_version': '3.0.0', 'via_device_id': , }), @@ -245,7 +238,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'qwerty1234567', - 'suggested_area': None, 'sw_version': '1.1.1', 'via_device_id': , }), @@ -276,7 +268,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'qwerty123', - 'suggested_area': None, 'sw_version': '1.0.1', 'via_device_id': , }), @@ -307,7 +298,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234', - 'suggested_area': None, 'sw_version': '1.0.1', 'via_device_id': , }), diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index fe330b82ca76f..212535862f5eb 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -129,7 +128,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -229,7 +227,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -331,7 +328,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -433,7 +429,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -472,7 +467,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -511,7 +505,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -550,7 +543,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -589,7 +581,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -628,7 +619,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -734,7 +724,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -773,7 +762,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index 20bf56ef9c443..eac595cc0e9dc 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -67,7 +66,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -106,7 +104,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -145,7 +142,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -184,7 +180,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -277,7 +272,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -372,7 +366,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -411,7 +404,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -450,7 +442,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -489,7 +480,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -528,7 +518,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -636,7 +625,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index a47de22f68b5a..6aa25e0763a17 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -163,7 +162,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -252,7 +250,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -438,7 +435,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -624,7 +620,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -663,7 +658,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -702,7 +696,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -792,7 +785,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -882,7 +874,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1245,7 +1236,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1284,7 +1274,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1323,7 +1312,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index edd2eee8b1f11..8947ac4042400 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -113,7 +112,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -198,7 +196,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -283,7 +280,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -368,7 +364,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -407,7 +402,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -446,7 +440,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -531,7 +524,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -616,7 +608,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -702,7 +693,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -787,7 +777,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -826,7 +815,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/webostv/snapshots/test_media_player.ambr b/tests/components/webostv/snapshots/test_media_player.ambr index 9c097b166ec0a..d0a1142618af2 100644 --- a/tests/components/webostv/snapshots/test_media_player.ambr +++ b/tests/components/webostv/snapshots/test_media_player.ambr @@ -63,7 +63,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': 'major.minor', 'via_device_id': None, }) diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 67f6baf45bbfe..38f125ad71261 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -75,7 +75,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -157,7 +156,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -243,7 +241,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -325,7 +322,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -407,7 +403,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -488,7 +483,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -569,7 +563,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -650,7 +643,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -731,7 +723,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -864,7 +855,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/withings/snapshots/test_init.ambr b/tests/components/withings/snapshots/test_init.ambr index ec711def829d6..88d9ff94b1dff 100644 --- a/tests/components/withings/snapshots/test_init.ambr +++ b/tests/components/withings/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index d8a29ed7c48c0..26f8817fa067a 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -80,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index 877c8baa93e51..5503b9a733dee 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -88,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -182,7 +181,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 6cfbe1de5d405..dc8a2f09445f4 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -90,7 +90,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -322,7 +321,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -416,7 +414,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.99.0b1', 'via_device_id': None, }) @@ -510,7 +507,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.99.0b1', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index c32bc314cc01d..09c86d81d4450 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -81,7 +81,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -166,7 +165,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -252,7 +250,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -338,7 +335,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wmspro/snapshots/test_cover.ambr b/tests/components/wmspro/snapshots/test_cover.ambr index 53b2f6205cb9c..026785c9e1c37 100644 --- a/tests/components/wmspro/snapshots/test_cover.ambr +++ b/tests/components/wmspro/snapshots/test_cover.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_init.ambr b/tests/components/wmspro/snapshots/test_init.ambr index 147d66f2b6983..9d60cf8c907fa 100644 --- a/tests/components/wmspro/snapshots/test_init.ambr +++ b/tests/components/wmspro/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '19239', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '19239', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -159,7 +155,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -192,7 +187,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -225,7 +219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '116682', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -258,7 +251,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '172555', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -291,7 +283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '18894', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -324,7 +315,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '230952', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -357,7 +347,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '284942', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -390,7 +379,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '328518', - 'suggested_area': 'Alle', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_light.ambr b/tests/components/wmspro/snapshots/test_light.ambr index d6ccebfb5eaa0..694fb2d51e410 100644 --- a/tests/components/wmspro/snapshots/test_light.ambr +++ b/tests/components/wmspro/snapshots/test_light.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_scene.ambr b/tests/components/wmspro/snapshots/test_scene.ambr index b5dddb368c986..97f47dc7f15e3 100644 --- a/tests/components/wmspro/snapshots/test_scene.ambr +++ b/tests/components/wmspro/snapshots/test_scene.ambr @@ -41,7 +41,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '42581', - 'suggested_area': 'Raum 0', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wolflink/snapshots/test_sensor.ambr b/tests/components/wolflink/snapshots/test_sensor.ambr index c5b23cc8e79ca..3d6e3fea3b51c 100644 --- a/tests/components/wolflink/snapshots/test_sensor.ambr +++ b/tests/components/wolflink/snapshots/test_sensor.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/wyoming/test_devices.py b/tests/components/wyoming/test_devices.py index 24423264f93cc..d03f2622c7156 100644 --- a/tests/components/wyoming/test_devices.py +++ b/tests/components/wyoming/test_devices.py @@ -8,13 +8,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import area_registry as ar, device_registry as dr async def test_device_registry_info( hass: HomeAssistant, satellite_device: SatelliteDevice, satellite_config_entry: ConfigEntry, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test info in device registry.""" @@ -26,7 +27,7 @@ async def test_device_registry_info( ) assert device is not None assert device.name == "Test Satellite" - assert device.suggested_area == "Office" + assert device.area_id == area_registry.async_get_area_by_name("Office").id # Check associated entities assist_in_progress_id = satellite_device.get_assist_in_progress_entity_id(hass) diff --git a/tests/components/yale/snapshots/test_binary_sensor.ambr b/tests/components/yale/snapshots/test_binary_sensor.ambr index 9db0d760efb11..df0e604c55042 100644 --- a/tests/components/yale/snapshots/test_binary_sensor.ambr +++ b/tests/components/yale/snapshots/test_binary_sensor.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'tmt100 Name', 'sw_version': '3.1.0-HYDRC75+201909251139', 'via_device_id': None, }) diff --git a/tests/components/yale/snapshots/test_lock.ambr b/tests/components/yale/snapshots/test_lock.ambr index 00653a9b0c139..dd2faa8b69e80 100644 --- a/tests/components/yale/snapshots/test_lock.ambr +++ b/tests/components/yale/snapshots/test_lock.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'online_with_doorsense Name', 'sw_version': 'undefined-4.3.0-1.8.14', 'via_device_id': None, }) diff --git a/tests/helpers/snapshots/test_entity_platform.ambr b/tests/helpers/snapshots/test_entity_platform.ambr index 55ff772e08ee9..ce0abffd03c57 100644 --- a/tests/helpers/snapshots/test_entity_platform.ambr +++ b/tests/helpers/snapshots/test_entity_platform.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Heliport', 'sw_version': 'test-sw', 'via_device_id': , }) @@ -68,7 +67,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Heliport', 'sw_version': 'test-sw', 'via_device_id': , }) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index a66684c94e37a..4247da296fd65 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -107,7 +107,6 @@ async def test_get_or_create_returns_same_entry( assert entry3.model == "model" assert entry3.name == "name" assert entry3.sw_version == "sw-version" - assert entry3.suggested_area == "Game Room" assert entry3.area_id == game_room_area.id await hass.async_block_till_done() @@ -409,7 +408,6 @@ async def test_loading_from_storage( name="name", primary_config_entry=mock_config_entry.entry_id, serial_number="serial_no", - suggested_area=None, # Not stored sw_version="version", ) assert isinstance(entry.config_entries, set) @@ -2509,13 +2507,13 @@ async def test_loading_saving_data( # Ensure a save/load cycle does not keep suggested area new_kitchen_light = registry2.async_get_device(identifiers={("hue", "999")}) - assert orig_kitchen_light.suggested_area == "Kitchen" + assert orig_kitchen_light.area_id == "kitchen" - orig_kitchen_light_witout_suggested_area = device_registry.async_update_device( + orig_kitchen_light_without_suggested_area = device_registry.async_update_device( orig_kitchen_light.id, suggested_area=None ) - assert orig_kitchen_light_witout_suggested_area.suggested_area is None - assert orig_kitchen_light_witout_suggested_area == new_kitchen_light + assert orig_kitchen_light_without_suggested_area.area_id == "kitchen" + assert orig_kitchen_light_without_suggested_area == new_kitchen_light async def test_no_unnecessary_changes( @@ -3225,7 +3223,6 @@ async def test_update_suggested_area( connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bla", "123")}, ) - assert not entry.suggested_area assert entry.area_id is None suggested_area = "Pool" @@ -3237,7 +3234,6 @@ async def test_update_suggested_area( assert mock_save.call_count == 1 assert updated_entry != entry - assert updated_entry.suggested_area == suggested_area pool_area = area_registry.async_get_area_by_name("Pool") assert pool_area is not None @@ -3267,7 +3263,7 @@ async def test_update_suggested_area( assert len(update_events) == 2 assert mock_save_2.call_count == 0 assert updated_entry != entry - assert updated_entry.suggested_area == "Other" + assert updated_entry.area_id == pool_area.id async def test_cleanup_device_registry( @@ -3475,7 +3471,6 @@ async def test_restore_device( name=None, primary_config_entry=entry_id, serial_number=None, - suggested_area=None, sw_version=None, ) # This will restore the original device, user customizations of @@ -4905,3 +4900,36 @@ async def test_connections_validator() -> None: """Test checking connections validator.""" with pytest.raises(ValueError, match="Invalid mac address format"): dr.DeviceEntry(connections={(dr.CONNECTION_NETWORK_MAC, "123456ABCDEF")}) + + +async def test_suggested_area_deprecation( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + area_registry: ar.AreaRegistry, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Make sure we do not duplicate entries.""" + entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="name", + manufacturer="manufacturer", + model="model", + suggested_area="Game Room", + ) + + game_room_area = area_registry.async_get_area_by_name("Game Room") + assert game_room_area is not None + assert len(area_registry.areas) == 1 + + assert len(device_registry.devices) == 1 + assert entry.area_id == game_room_area.id + assert entry.suggested_area == "Game Room" + + assert ( + "The deprecated function suggested_area was called. It will be removed in " + "HA Core 2026.9. Use code which ignores suggested_area instead" + ) in caplog.text diff --git a/tests/syrupy.py b/tests/syrupy.py index e028d5839cbff..642e5a519b2bd 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -173,6 +173,8 @@ def _serializable_device_registry_entry( if serialized["primary_config_entry"] is not None: serialized["primary_config_entry"] = ANY serialized.pop("_cache") + # This can be removed when suggested_area is removed from DeviceEntry + serialized.pop("_suggested_area") return cls._remove_created_and_modified_at(serialized) @classmethod From b521b1e64c72554a5e7a8d54839c807c31c35893 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Aug 2025 14:54:58 +0200 Subject: [PATCH 029/247] Make device suggested_area only influence new devices (#149758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- homeassistant/helpers/device_registry.py | 43 +++++++++++------- tests/helpers/test_device_registry.py | 57 +++++++++++++++++------- 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index d3866d8c9c340..72d0cf651f2b3 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -906,7 +906,19 @@ def async_get_or_create( if device is None: deleted_device = self.deleted_devices.get_entry(identifiers, connections) if deleted_device is None: - device = DeviceEntry(is_new=True) + area_id: str | None = None + if ( + suggested_area is not None + and suggested_area is not UNDEFINED + and suggested_area != "" + ): + # Circular dep + from . import area_registry as ar # noqa: PLC0415 + + area = ar.async_get(self.hass).async_get_or_create(suggested_area) + area_id = area.id + device = DeviceEntry(is_new=True, area_id=area_id) + else: self.deleted_devices.pop(deleted_device.id) device = deleted_device.to_device_entry( @@ -961,7 +973,7 @@ def async_get_or_create( model_id=model_id, name=name, serial_number=serial_number, - suggested_area=suggested_area, + _suggested_area=suggested_area, sw_version=sw_version, via_device_id=via_device_id, ) @@ -1000,6 +1012,10 @@ def async_update_device( # noqa: C901 remove_config_entry_id: str | UndefinedType = UNDEFINED, remove_config_subentry_id: str | None | UndefinedType = UNDEFINED, serial_number: str | None | UndefinedType = UNDEFINED, + # _suggested_area is used internally by the device registry and must + # not be set by integrations. + _suggested_area: str | None | UndefinedType = UNDEFINED, + # suggested_area is deprecated and will be removed in 2026.9 suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, @@ -1065,19 +1081,6 @@ def async_update_device( # noqa: C901 "Cannot define both merge_identifiers and new_identifiers" ) - if ( - suggested_area is not None - and suggested_area is not UNDEFINED - and suggested_area != "" - and area_id is UNDEFINED - and old.area_id is None - ): - # Circular dep - from . import area_registry as ar # noqa: PLC0415 - - area = ar.async_get(self.hass).async_get_or_create(suggested_area) - area_id = area.id - if add_config_entry_id is not UNDEFINED: if add_config_subentry_id is UNDEFINED: # Interpret not specifying a subentry as None (the main entry) @@ -1155,6 +1158,16 @@ def async_update_device( # noqa: C901 new_values["config_entries_subentries"] = config_entries_subentries old_values["config_entries_subentries"] = old.config_entries_subentries + if suggested_area is not UNDEFINED: + report_usage( + "passes a suggested_area to device_registry.async_update device", + core_behavior=ReportBehavior.LOG, + breaks_in_ha_version="2026.9.0", + ) + + if _suggested_area is not UNDEFINED: + suggested_area = _suggested_area + added_connections: set[tuple[str, str]] | None = None added_identifiers: set[tuple[str, str]] | None = None diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 4247da296fd65..d056c25fc3be4 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3210,20 +3210,35 @@ async def test_update_remove_config_subentries( } +@pytest.mark.parametrize( + ("initial_area", "device_area_id", "number_of_areas"), + [ + (None, None, 0), + ("Living Room", "living_room", 1), + ], +) async def test_update_suggested_area( hass: HomeAssistant, device_registry: dr.DeviceRegistry, area_registry: ar.AreaRegistry, mock_config_entry: MockConfigEntry, + initial_area: str | None, + device_area_id: str | None, + number_of_areas: int, ) -> None: - """Verify that we can update the suggested area version of a device.""" + """Verify that we can update the suggested area of a device. + + Updating the suggested area of a device should not create a new area, nor should + it change the area_id of the device. + """ update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) entry = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bla", "123")}, + suggested_area=initial_area, ) - assert entry.area_id is None + assert entry.area_id == device_area_id suggested_area = "Pool" @@ -3232,26 +3247,24 @@ async def test_update_suggested_area( entry.id, suggested_area=suggested_area ) - assert mock_save.call_count == 1 + # Check the device registry was not saved + assert mock_save.call_count == 0 assert updated_entry != entry + assert updated_entry.area_id == device_area_id - pool_area = area_registry.async_get_area_by_name("Pool") - assert pool_area is not None - assert updated_entry.area_id == pool_area.id - assert len(area_registry.areas) == 1 + # Check we did not create an area + pool_area = area_registry.async_get_area_by_name(suggested_area) + assert pool_area is None + assert updated_entry.area_id == device_area_id + assert len(area_registry.areas) == number_of_areas await hass.async_block_till_done() - assert len(update_events) == 2 + assert len(update_events) == 1 assert update_events[0].data == { "action": "create", "device_id": entry.id, } - assert update_events[1].data == { - "action": "update", - "device_id": entry.id, - "changes": {"area_id": None, "suggested_area": None}, - } # Do not save or fire the event if the suggested # area does not result in a change of area @@ -3260,10 +3273,10 @@ async def test_update_suggested_area( updated_entry = device_registry.async_update_device( entry.id, suggested_area="Other" ) - assert len(update_events) == 2 + assert len(update_events) == 1 assert mock_save_2.call_count == 0 assert updated_entry != entry - assert updated_entry.area_id == pool_area.id + assert updated_entry.area_id == device_area_id async def test_cleanup_device_registry( @@ -3397,11 +3410,13 @@ async def test_cleanup_entity_registry_change( assert len(mock_call.mock_calls) == 2 +@pytest.mark.parametrize("initial_area", [None, "12345A"]) @pytest.mark.usefixtures("freezer") async def test_restore_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_config_entry_with_subentries: MockConfigEntry, + initial_area: str | None, ) -> None: """Make sure device id is stable.""" entry_id = mock_config_entry_with_subentries.entry_id @@ -3428,7 +3443,7 @@ async def test_restore_device( # Apply user customizations entry = device_registry.async_update_device( entry.id, - area_id="12345A", + area_id=initial_area, disabled_by=dr.DeviceEntryDisabler.USER, labels={"label1", "label2"}, name_by_user="Test Friendly Name", @@ -3493,7 +3508,7 @@ async def test_restore_device( via_device="via_device_id_new", ) assert entry3 == dr.DeviceEntry( - area_id="12345A", + area_id=initial_area, config_entries={entry_id}, config_entries_subentries={entry_id: {subentry_id}}, configuration_url="http://config_url_new.bla", @@ -4933,3 +4948,11 @@ async def test_suggested_area_deprecation( "The deprecated function suggested_area was called. It will be removed in " "HA Core 2026.9. Use code which ignores suggested_area instead" ) in caplog.text + + device_registry.async_update_device(entry.id, suggested_area="TV Room") + + assert ( + "Detected code that passes a suggested_area to device_registry.async_update " + "device. This will stop working in Home Assistant 2026.9.0, please report " + "this issue" + ) in caplog.text From c59fbdeec12a25221d673d47031321a7f524c08c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:52:17 -0400 Subject: [PATCH 030/247] Fix ZHA ContextVar deprecation by passing config_entry (#149748) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com> Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> Co-authored-by: TheJulianJES <6409465+TheJulianJES@users.noreply.github.com> --- homeassistant/components/zha/update.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index 062581fd25992..867e4ff2dd36c 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -58,7 +58,7 @@ async def async_setup_entry( zha_data = get_zha_data(hass) if zha_data.update_coordinator is None: zha_data.update_coordinator = ZHAFirmwareUpdateCoordinator( - hass, get_zha_gateway(hass).application_controller + hass, config_entry, get_zha_gateway(hass).application_controller ) entities_to_create = zha_data.platforms[Platform.UPDATE] @@ -79,12 +79,16 @@ class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disa """Firmware update coordinator that broadcasts updates network-wide.""" def __init__( - self, hass: HomeAssistant, controller_application: ControllerApplication + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + controller_application: ControllerApplication, ) -> None: """Initialize the coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="ZHA firmware update coordinator", update_method=self.async_update_data, ) From a095631f4ff58200077506f316e2c61f05828a9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 12:23:24 -1000 Subject: [PATCH 031/247] Bump aioesphomeapi to 37.2.2 (#149755) --- 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 5a7c9a5f92773..6bf164aa9bc6c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.2.0", + "aioesphomeapi==37.2.2", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index eb2d44e24f698..ca03a24607091 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.2.0 +aioesphomeapi==37.2.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ff3286b03b15..ce6a8857fe343 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.2.0 +aioesphomeapi==37.2.2 # homeassistant.components.flo aioflo==2021.11.0 From 6857e87b30c2d92135cf54c28e48e1336d9bdcd0 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 31 Jul 2025 13:04:23 -0600 Subject: [PATCH 032/247] Bump pylitterbot to 2024.2.3 (#149763) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 33addd85ba2ec..e67c681ac5360 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_push", "loggers": ["pylitterbot"], "quality_scale": "bronze", - "requirements": ["pylitterbot==2024.2.2"] + "requirements": ["pylitterbot==2024.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index ca03a24607091..116e383ec7769 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2122,7 +2122,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.3 # homeassistant.components.litterrobot -pylitterbot==2024.2.2 +pylitterbot==2024.2.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce6a8857fe343..4c7dc1dbe632b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1767,7 +1767,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.3 # homeassistant.components.litterrobot -pylitterbot==2024.2.2 +pylitterbot==2024.2.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.24.0 From c8069a383eda66d1f68adafd615a479b5de60ea7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 1 Aug 2025 00:09:20 +0200 Subject: [PATCH 033/247] Bump motionblinds to 0.6.30 (#149764) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index eca520d8946f8..ac5390f5c649e 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.29"] + "requirements": ["motionblinds==0.6.30"] } diff --git a/requirements_all.txt b/requirements_all.txt index 116e383ec7769..0c9057538a991 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1458,7 +1458,7 @@ monzopy==1.5.1 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.29 +motionblinds==0.6.30 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c7dc1dbe632b..4119c5b2e987e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1250,7 +1250,7 @@ monzopy==1.5.1 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.29 +motionblinds==0.6.30 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 From 6b93f6d75c6e48069277ce4df22e36e93eb9d06d Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 1 Aug 2025 00:07:56 +0200 Subject: [PATCH 034/247] Hide configuration URL when Uptime Kuma is installed locally (#149781) --- homeassistant/components/uptime_kuma/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/uptime_kuma/sensor.py b/homeassistant/components/uptime_kuma/sensor.py index c76fbcae04c3e..b499c67da1652 100644 --- a/homeassistant/components/uptime_kuma/sensor.py +++ b/homeassistant/components/uptime_kuma/sensor.py @@ -162,7 +162,11 @@ def __init__( name=coordinator.data[monitor].monitor_name, identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor!s}")}, manufacturer="Uptime Kuma", - configuration_url=coordinator.config_entry.data[CONF_URL], + configuration_url=( + None + if "127.0.0.1" in (url := coordinator.config_entry.data[CONF_URL]) + else url + ), sw_version=coordinator.api.version.version, ) From b60b1fc0c6af960b4681f086d234264acc2264a5 Mon Sep 17 00:00:00 2001 From: Jamin Date: Fri, 1 Aug 2025 14:37:45 -0500 Subject: [PATCH 035/247] Bump VoIP utils to 0.3.4 (#149786) --- homeassistant/components/voip/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index 0b533795a2c59..fe855159d5567 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["voip_utils"], "quality_scale": "internal", - "requirements": ["voip-utils==0.3.3"] + "requirements": ["voip-utils==0.3.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0c9057538a991..7a3ef0700b213 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3057,7 +3057,7 @@ venstarcolortouch==0.21 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.3 +voip-utils==0.3.4 # homeassistant.components.volkszaehler volkszaehler==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4119c5b2e987e..9d669f863f8a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2525,7 +2525,7 @@ venstarcolortouch==0.21 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.3 +voip-utils==0.3.4 # homeassistant.components.volvo volvocarsapi==0.4.1 From 9649fbc1899e759ad565b406cb44927cdc162fc9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 1 Aug 2025 09:13:53 +0200 Subject: [PATCH 036/247] Fix tuya light supported color modes (#149793) Co-authored-by: Erik --- homeassistant/components/tuya/light.py | 34 ++-- tests/components/tuya/__init__.py | 6 + .../tuya/fixtures/tyndj_pyakuuoc.json | 145 ++++++++++++++++++ .../components/tuya/snapshots/test_light.ambr | 56 +++++++ .../tuya/snapshots/test_sensor.ambr | 101 ++++++++++++ .../tuya/snapshots/test_switch.ambr | 48 ++++++ 6 files changed, 377 insertions(+), 13 deletions(-) create mode 100644 tests/components/tuya/fixtures/tyndj_pyakuuoc.json diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index cb7555c38d8a4..7b73e8259004e 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -16,6 +16,7 @@ ColorMode, LightEntity, LightEntityDescription, + color_supported, filter_supported_color_modes, ) from homeassistant.const import EntityCategory @@ -530,19 +531,6 @@ def __init__( description.brightness_min, dptype=DPType.INTEGER ) - if int_type := self.find_dpcode( - description.color_temp, dptype=DPType.INTEGER, prefer_function=True - ): - self._color_temp = int_type - color_modes.add(ColorMode.COLOR_TEMP) - # If entity does not have color_temp, check if it has work_mode "white" - elif color_mode_enum := self.find_dpcode( - description.color_mode, dptype=DPType.ENUM, prefer_function=True - ): - if WorkMode.WHITE.value in color_mode_enum.range: - color_modes.add(ColorMode.WHITE) - self._white_color_mode = ColorMode.WHITE - if ( dpcode := self.find_dpcode(description.color_data, prefer_function=True) ) and self.get_dptype(dpcode) == DPType.JSON: @@ -568,6 +556,26 @@ def __init__( ): self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2 + # Check if the light has color temperature + if int_type := self.find_dpcode( + description.color_temp, dptype=DPType.INTEGER, prefer_function=True + ): + self._color_temp = int_type + color_modes.add(ColorMode.COLOR_TEMP) + # If light has color but does not have color_temp, check if it has + # work_mode "white" + elif ( + color_supported(color_modes) + and ( + color_mode_enum := self.find_dpcode( + description.color_mode, dptype=DPType.ENUM, prefer_function=True + ) + ) + and WorkMode.WHITE.value in color_mode_enum.range + ): + color_modes.add(ColorMode.WHITE) + self._white_color_mode = ColorMode.WHITE + self._attr_supported_color_modes = filter_supported_color_modes(color_modes) if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 039b8f292905b..d793b87854a45 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -149,6 +149,12 @@ Platform.SELECT, Platform.SWITCH, ], + "tyndj_pyakuuoc": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, + ], "wk_air_conditioner": [ # https://github.com/home-assistant/core/issues/146263 Platform.CLIMATE, diff --git a/tests/components/tuya/fixtures/tyndj_pyakuuoc.json b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json new file mode 100644 index 0000000000000..973cecabc0b02 --- /dev/null +++ b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json @@ -0,0 +1,145 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "1753247726209KOaaPc", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfdb773e4ae317e3915h2i", + "name": "Solar zijpad", + "category": "tyndj", + "product_id": "pyakuuoc", + "product_name": "Solar flood light App panel", + "online": false, + "sub": true, + "time_zone": "+08:00", + "active_time": "2023-03-08T13:24:06+00:00", + "create_time": "2023-03-08T13:24:06+00:00", + "update_time": "2023-03-08T13:24:06+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "countdown": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "switch_save_energy": { + "type": "Boolean", + "value": {} + }, + "device_mode": { + "type": "Enum", + "value": { + "range": ["manual", "auto"] + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "countdown": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "switch_save_energy": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "device_mode": { + "type": "Enum", + "value": { + "range": ["manual", "auto"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value": 10, + "scene_data": "", + "countdown": 0, + "switch_save_energy": false, + "battery_percentage": 0, + "device_mode": "manual", + "battery_state": "low" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 5fcf58dda6d1b..ec8e663f62ca6 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -249,3 +249,59 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.solar_zijpad', + 'has_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': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfdb773e4ae317e3915h2iswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.solar_zijpad', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 57e73eccda507..80051a08396cc 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -2233,6 +2233,107 @@ 'state': '0.0', }) # --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solar_zijpad_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2ibattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Solar zijpad Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solar_zijpad_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solar_zijpad_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2ibattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad Battery state', + }), + 'context': , + 'entity_id': 'sensor.solar_zijpad_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 71aa05329aa53..e21fe9c91bd0c 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1161,6 +1161,54 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][switch.solar_zijpad_energy_saving-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.solar_zijpad_energy_saving', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Energy saving', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_saving', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2iswitch_save_energy', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][switch.solar_zijpad_energy_saving-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad Energy saving', + }), + 'context': , + 'entity_id': 'switch.solar_zijpad_energy_saving', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[wk_air_conditioner][switch.clima_cucina_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 35d0c254a2acd5490793cba80f636379a887c7a4 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 20:35:48 +0200 Subject: [PATCH 037/247] Fix descriptions for template number fields (#149804) --- homeassistant/components/template/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index d29bfbeb3fb85..4d6714ca0ec64 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -278,10 +278,10 @@ "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", "state": "Template for the number's current value.", - "step": "Template for the number's increment/decrement step.", + "step": "Defines the number's increment/decrement step.", "set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.", - "max": "Template for the number's maximum value.", - "min": "Template for the number's minimum value.", + "max": "Defines the number's maximum value.", + "min": "Defines the number's minimum value.", "unit_of_measurement": "Defines the unit of measurement of the number, if any." }, "sections": { From 6877fdaf5b6e27bc9acb3aee3a6bb47ec2696889 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Sat, 2 Aug 2025 22:17:13 +0200 Subject: [PATCH 038/247] Add scopes in config flow auth request for Volvo integration (#149813) --- homeassistant/components/volvo/config_flow.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/volvo/config_flow.py b/homeassistant/components/volvo/config_flow.py index 05d19fd1d26c4..f187d751a2dce 100644 --- a/homeassistant/components/volvo/config_flow.py +++ b/homeassistant/components/volvo/config_flow.py @@ -9,6 +9,7 @@ import voluptuous as vol from volvocarsapi.api import VolvoCarsApi from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle +from volvocarsapi.scopes import DEFAULT_SCOPES from homeassistant.config_entries import ( SOURCE_REAUTH, @@ -54,6 +55,13 @@ def __init__(self) -> None: self._vehicles: list[VolvoCarsVehicle] = [] self._config_data: dict = {} + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return super().extra_authorize_data | { + "scope": " ".join(DEFAULT_SCOPES), + } + @property def logger(self) -> logging.Logger: """Return logger.""" From 214940d04f2feb82210d33919275c62dac30f068 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 19:30:59 +0200 Subject: [PATCH 039/247] Add translation for `absolute_humidity` device class to `template` (#149814) --- homeassistant/components/template/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 4d6714ca0ec64..cdaeacbe842d2 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -901,6 +901,7 @@ }, "sensor_device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "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%]", From 7e5cf17cf463ef3422fdd51a9b2918c3d17ef31e Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 14:32:14 +0200 Subject: [PATCH 040/247] Add translation for `absolute_humidity` device class to `random` (#149815) --- homeassistant/components/random/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index d57f2dc8eec15..1f28000d0f420 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -82,6 +82,7 @@ }, "sensor_device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "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%]", From 3a8d962d34a0be61533000cdbe9f27a38abeeb33 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 15:57:47 +0200 Subject: [PATCH 041/247] Add translation for `absolute_humidity` device class to `mqtt` (#149818) --- homeassistant/components/mqtt/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 40215b0f2c603..0e248cfd2d2b9 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -1104,6 +1104,7 @@ }, "device_class_sensor": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", From 6a17a12be54ea1fdf3538253655e60594a8b0e3f Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 21:36:10 +0200 Subject: [PATCH 042/247] Update reference for `volatile_organic_compounds_parts` in `template` (#149831) --- homeassistant/components/template/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index cdaeacbe842d2..be5fb1866ea6d 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -949,7 +949,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%]", From 1d383e80a457f5267fbd74100a5451431b97b366 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sat, 2 Aug 2025 19:01:02 +0100 Subject: [PATCH 043/247] Fix initialisation of Apps and Radios list for Squeezebox (#149834) --- .../components/squeezebox/browse_media.py | 54 ++++++++++++++----- .../components/squeezebox/media_player.py | 5 ++ .../squeezebox/test_media_player.py | 8 +-- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index bab4f90c6d13a..4f2a1fa7aa570 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -4,6 +4,7 @@ import contextlib from dataclasses import dataclass, field +import logging from typing import Any from pysqueezebox import Player @@ -21,6 +22,8 @@ from .const import DOMAIN, UNPLAYABLE_TYPES +_LOGGER = logging.getLogger(__name__) + LIBRARY = [ "favorites", "artists", @@ -138,18 +141,42 @@ def __post_init__(self) -> None: self.squeezebox_id_by_type.update(SQUEEZEBOX_ID_BY_TYPE) self.media_type_to_squeezebox.update(MEDIA_TYPE_TO_SQUEEZEBOX) - -def _add_new_command_to_browse_data( - browse_data: BrowseData, cmd: str | MediaType, type: str -) -> None: - """Add items to maps for new apps or radios.""" - browse_data.media_type_to_squeezebox[cmd] = cmd - browse_data.squeezebox_id_by_type[cmd] = type - browse_data.content_type_media_class[cmd] = { - "item": MediaClass.DIRECTORY, - "children": MediaClass.TRACK, - } - browse_data.content_type_to_child_type[cmd] = MediaType.TRACK + def add_new_command(self, cmd: str | MediaType, type: str) -> None: + """Add items to maps for new apps or radios.""" + self.known_apps_radios.add(cmd) + self.media_type_to_squeezebox[cmd] = cmd + self.squeezebox_id_by_type[cmd] = type + self.content_type_media_class[cmd] = { + "item": MediaClass.DIRECTORY, + "children": MediaClass.TRACK, + } + self.content_type_to_child_type[cmd] = MediaType.TRACK + + async def async_init(self, player: Player, browse_limit: int) -> None: + """Initialize known apps and radios from the player.""" + + cmd = ["apps", 0, browse_limit] + result = await player.async_query(*cmd) + for app in result["appss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) + cmd = ["radios", 0, browse_limit] + result = await player.async_query(*cmd) + for app in result["radioss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) def _build_response_apps_radios_category( @@ -292,8 +319,7 @@ async def build_item_response( app_cmd = "app-" + item["cmd"] if app_cmd not in browse_data.known_apps_radios: - browse_data.known_apps_radios.add(app_cmd) - _add_new_command_to_browse_data(browse_data, app_cmd, "item_id") + browse_data.add_new_command(app_cmd, "item_id") child_media = _build_response_apps_radios_category( browse_data=browse_data, cmd=app_cmd, item=item diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 0dbc1b96b0ce7..49aad4fd6984a 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -311,6 +311,11 @@ def state(self) -> MediaPlayerState | None: ) return None + async def async_added_to_hass(self) -> None: + """Call when entity is added to hass.""" + await super().async_added_to_hass() + await self._browse_data.async_init(self._player, self.browse_limit) + async def async_will_remove_from_hass(self) -> None: """Remove from list of known players when removed from hass.""" self.coordinator.config_entry.runtime_data.known_player_ids.remove( diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index 440f682370be0..6e3e5be04592d 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -765,9 +765,7 @@ async def test_squeezebox_call_query( }, blocking=True, ) - configured_player.async_query.assert_called_once_with( - "test_command", "param1", "param2" - ) + configured_player.async_query.assert_called_with("test_command", "param1", "param2") async def test_squeezebox_call_method( @@ -784,9 +782,7 @@ async def test_squeezebox_call_method( }, blocking=True, ) - configured_player.async_query.assert_called_once_with( - "test_command", "param1", "param2" - ) + configured_player.async_query.assert_called_with("test_command", "param1", "param2") async def test_squeezebox_invalid_state( From 8d0ceff652c42212c6b04866d4278b2b9ab32b4f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 2 Aug 2025 23:07:16 +0200 Subject: [PATCH 044/247] Fix Z-Wave config entry state conditions in listen task (#149841) --- homeassistant/components/zwave_js/__init__.py | 19 ++-- tests/components/zwave_js/conftest.py | 10 +-- tests/components/zwave_js/test_init.py | 86 ++++++++++++++++--- 3 files changed, 88 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 360969e83d474..52a5a1b7388a1 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1074,23 +1074,32 @@ async def client_listen( try: await client.listen(driver_ready) except BaseZwaveJSServerError as err: - if entry.state is not ConfigEntryState.LOADED: + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: raise LOGGER.error("Client listen failed: %s", err) except Exception as err: # We need to guard against unknown exceptions to not crash this task. LOGGER.exception("Unexpected exception: %s", err) - if entry.state is not ConfigEntryState.LOADED: + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: raise + if hass.is_stopping or entry.state is ConfigEntryState.UNLOAD_IN_PROGRESS: + return + + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: + raise HomeAssistantError("Listen task ended unexpectedly") + # The entry needs to be reloaded since a new driver state # will be acquired on reconnect. # All model instances will be replaced when the new state is acquired. - if not hass.is_stopping: - if entry.state is not ConfigEntryState.LOADED: - raise HomeAssistantError("Listen task ended unexpectedly") + if entry.state.recoverable: LOGGER.debug("Disconnected from server. Reloading integration") hass.config_entries.async_schedule_reload(entry.entry_id) + else: + LOGGER.error( + "Disconnected from server. Cannot recover entry %s", + entry.title, + ) async def async_unload_entry(hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> bool: diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 3c07869d5b735..eef92a7eb0abd 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -565,12 +565,6 @@ def mock_listen_block_fixture() -> asyncio.Event: return asyncio.Event() -@pytest.fixture(name="listen_result") -def listen_result_fixture() -> asyncio.Future[None]: - """Mock a listen result.""" - return asyncio.Future() - - @pytest.fixture(name="client") def mock_client_fixture( controller_state: dict[str, Any], @@ -578,7 +572,6 @@ def mock_client_fixture( version_state: dict[str, Any], log_config_state: dict[str, Any], listen_block: asyncio.Event, - listen_result: asyncio.Future[None], ): """Mock a client.""" with patch( @@ -587,15 +580,16 @@ def mock_client_fixture( client = client_class.return_value async def connect(): + listen_block.clear() await asyncio.sleep(0) client.connected = True async def listen(driver_ready: asyncio.Event) -> None: driver_ready.set() await listen_block.wait() - await listen_result async def disconnect(): + listen_block.set() client.connected = False client.connect = AsyncMock(side_effect=connect) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index d9b3f392dd6fd..4decb061ad0fd 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -196,19 +196,24 @@ async def test_listen_done_during_setup_before_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, ) -> None: """Test listen task finishing during setup before forward entry.""" + listen_result = asyncio.Future[None]() assert hass.state is CoreState.running + async def connect(): + await asyncio.sleep(0) + client.connected = True + async def listen(driver_ready: asyncio.Event) -> None: await listen_block.wait() await listen_result async_fire_time_changed(hass, fire_all=True) + client.connect.side_effect = connect client.listen.side_effect = listen hass.set_state(core_state) listen_block.set() @@ -229,9 +234,9 @@ async def test_not_connected_during_setup_after_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], ) -> None: """Test we handle not connected client during setup after forward entry.""" + listen_result = asyncio.Future[None]() async def send_command_side_effect(*args: Any, **kwargs: Any) -> None: """Mock send command.""" @@ -277,12 +282,12 @@ async def test_listen_done_during_setup_after_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, ) -> None: """Test listen task finishing during setup after forward entry.""" + listen_result = asyncio.Future[None]() assert hass.state is CoreState.running original_send_command_side_effect = client.async_send_command.side_effect @@ -320,16 +325,14 @@ async def listen(driver_ready: asyncio.Event) -> None: @pytest.mark.parametrize( - ("core_state", "final_config_entry_state", "disconnect_call_count"), + ("core_state", "disconnect_call_count"), [ ( CoreState.running, - ConfigEntryState.SETUP_RETRY, - 2, - ), # the reload will cause a disconnect call too + 1, + ), # the reload will cause a disconnect ( CoreState.stopping, - ConfigEntryState.LOADED, 0, ), # the home assistant stop event will handle the disconnect ], @@ -345,19 +348,33 @@ async def listen(driver_ready: asyncio.Event) -> None: async def test_listen_done_after_setup( hass: HomeAssistant, client: MagicMock, - integration: MockConfigEntry, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, - final_config_entry_state: ConfigEntryState, disconnect_call_count: int, ) -> None: """Test listen task finishing after setup.""" - config_entry = integration - assert config_entry.state is ConfigEntryState.LOADED + listen_result = asyncio.Future[None]() + + async def listen(driver_ready: asyncio.Event) -> None: + driver_ready.set() + await listen_block.wait() + await listen_result + + client.listen.side_effect = listen + + config_entry = MockConfigEntry( + domain="zwave_js", + data={"url": "ws://test.org", "data_collection_opted_in": True}, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.state is CoreState.running + assert config_entry.state is ConfigEntryState.LOADED assert client.disconnect.call_count == 0 hass.set_state(core_state) @@ -365,10 +382,51 @@ async def test_listen_done_after_setup( getattr(listen_result, listen_future_result_method)(listen_future_result) await hass.async_block_till_done() - assert config_entry.state is final_config_entry_state + assert config_entry.state is ConfigEntryState.LOADED assert client.disconnect.call_count == disconnect_call_count +async def test_listen_ending_before_cancelling_listen( + hass: HomeAssistant, + integration: MockConfigEntry, + listen_block: asyncio.Event, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test listen ending during unloading before cancelling the listen task.""" + config_entry = integration + + # We can't easily simulate the race condition where the listen task ends + # before getting cancelled by the config entry during unloading. + # Use mock_state to provoke the correct condition. + config_entry.mock_state(hass, ConfigEntryState.UNLOAD_IN_PROGRESS, None) + listen_block.set() + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.UNLOAD_IN_PROGRESS + assert not any(record.levelno == logging.ERROR for record in caplog.records) + + +async def test_listen_ending_unrecoverable_config_entry_state( + hass: HomeAssistant, + integration: MockConfigEntry, + listen_block: asyncio.Event, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test listen ending when the config entry has an unrecoverable state.""" + config_entry = integration + + with patch.object( + hass.config_entries, "async_unload_platforms", return_value=False + ): + await hass.config_entries.async_unload(config_entry.entry_id) + + listen_block.set() + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.FAILED_UNLOAD + assert "Disconnected from server. Cannot recover entry" in caplog.text + + @pytest.mark.usefixtures("client") @pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) async def test_new_entity_on_value_added( From c459ceba735414c31581affe6494260ceeb61a64 Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sat, 2 Aug 2025 19:44:01 +0200 Subject: [PATCH 045/247] Update `denonavr` to `1.1.2` (#149842) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index c5a1b9aeb63e3..8fea21b707ee3 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/denonavr", "iot_class": "local_push", "loggers": ["denonavr"], - "requirements": ["denonavr==1.1.1"], + "requirements": ["denonavr==1.1.2"], "ssdp": [ { "manufacturer": "Denon", diff --git a/requirements_all.txt b/requirements_all.txt index 7a3ef0700b213..29e88d0e38f54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -791,7 +791,7 @@ deluge-client==1.10.2 demetriek==1.3.0 # homeassistant.components.denonavr -denonavr==1.1.1 +denonavr==1.1.2 # homeassistant.components.devialet devialet==1.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d669f863f8a1..f27183abee410 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -691,7 +691,7 @@ deluge-client==1.10.2 demetriek==1.3.0 # homeassistant.components.denonavr -denonavr==1.1.1 +denonavr==1.1.2 # homeassistant.components.devialet devialet==1.5.7 From 138c19126b42df83e4ca9d4f4c100e542949bad9 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Sat, 2 Aug 2025 18:37:57 +0200 Subject: [PATCH 046/247] Fix Miele hob translation keys (#149865) --- homeassistant/components/miele/strings.json | 42 ++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index a4400ff26ebbe..90689a3d9cc1b 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -203,27 +203,27 @@ "plate": { "name": "Plate {plate_no}", "state": { - "power_step_0": "0", - "power_step_warm": "Warming", - "power_step_1": "1", - "power_step_2": "1\u2022", - "power_step_3": "2", - "power_step_4": "2\u2022", - "power_step_5": "3", - "power_step_6": "3\u2022", - "power_step_7": "4", - "power_step_8": "4\u2022", - "power_step_9": "5", - "power_step_10": "5\u2022", - "power_step_11": "6", - "power_step_12": "6\u2022", - "power_step_13": "7", - "power_step_14": "7\u2022", - "power_step_15": "8", - "power_step_16": "8\u2022", - "power_step_17": "9", - "power_step_18": "9\u2022", - "power_step_boost": "Boost" + "plate_step_0": "0", + "plate_step_warm": "Warming", + "plate_step_1": "1", + "plate_step_2": "1\u2022", + "plate_step_3": "2", + "plate_step_4": "2\u2022", + "plate_step_5": "3", + "plate_step_6": "3\u2022", + "plate_step_7": "4", + "plate_step_8": "4\u2022", + "plate_step_9": "5", + "plate_step_10": "5\u2022", + "plate_step_11": "6", + "plate_step_12": "6\u2022", + "plate_step_13": "7", + "plate_step_14": "7\u2022", + "plate_step_15": "8", + "plate_step_16": "8\u2022", + "plate_step_17": "9", + "plate_step_18": "9\u2022", + "plate_step_boost": "Boost" } }, "drying_step": { From c268e57ba77d49fd00d352489a5989e316fa77f9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 3 Aug 2025 21:46:40 +0200 Subject: [PATCH 047/247] Bump python-open-router to 0.3.1 (#149873) --- homeassistant/components/open_router/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/open_router/manifest.json b/homeassistant/components/open_router/manifest.json index fab62e7971c88..8f989e63189b3 100644 --- a/homeassistant/components/open_router/manifest.json +++ b/homeassistant/components/open_router/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["openai==1.93.3", "python-open-router==0.3.0"] + "requirements": ["openai==1.93.3", "python-open-router==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 29e88d0e38f54..fc2860b3bdda5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2481,7 +2481,7 @@ python-mpd2==3.1.1 python-mystrom==2.4.0 # homeassistant.components.open_router -python-open-router==0.3.0 +python-open-router==0.3.1 # homeassistant.components.swiss_public_transport python-opendata-transport==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f27183abee410..9e7194ff011b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2054,7 +2054,7 @@ python-mpd2==3.1.1 python-mystrom==2.4.0 # homeassistant.components.open_router -python-open-router==0.3.0 +python-open-router==0.3.1 # homeassistant.components.swiss_public_transport python-opendata-transport==0.5.0 From 89f6cfeb819e793c8d761c209a71a43ccc888f79 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 3 Aug 2025 11:23:01 +0200 Subject: [PATCH 048/247] Fix Z-Wave handling of driver ready event (#149879) --- homeassistant/components/zwave_js/__init__.py | 12 +- homeassistant/components/zwave_js/api.py | 39 +-- .../components/zwave_js/config_flow.py | 26 +- homeassistant/components/zwave_js/const.py | 4 - homeassistant/components/zwave_js/helpers.py | 55 +++- tests/components/zwave_js/test_api.py | 282 ++++++++---------- tests/components/zwave_js/test_config_flow.py | 8 +- tests/components/zwave_js/test_init.py | 35 +++ 8 files changed, 258 insertions(+), 203 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 52a5a1b7388a1..923cd776f9241 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -105,7 +105,6 @@ CONF_USB_PATH, CONF_USE_ADDON, DOMAIN, - DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_VALUE_UPDATED, LIB_LOGGER, @@ -136,6 +135,7 @@ from .services import async_setup_services CONNECT_TIMEOUT = 10 +DRIVER_READY_TIMEOUT = 60 CONFIG_SCHEMA = vol.Schema( { @@ -368,6 +368,16 @@ async def handle_logging_changed(_: Event | None = None) -> None: ) ) + # listen for driver ready event to reload the config entry + self.config_entry.async_on_unload( + driver.on( + "driver ready", + lambda _: self.hass.config_entries.async_schedule_reload( + self.config_entry.entry_id + ), + ) + ) + # listen for new nodes being added to the mesh self.config_entry.async_on_unload( controller.on( diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 0f75d8b46739c..b392b1c95cdde 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections.abc import Callable, Coroutine from contextlib import suppress import dataclasses @@ -87,7 +86,6 @@ CONF_DATA_COLLECTION_OPTED_IN, CONF_INSTALLER_MODE, DOMAIN, - DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, USER_AGENT, @@ -98,6 +96,7 @@ async_get_node_from_device_id, async_get_provisioning_entry_from_device_id, async_get_version_info, + async_wait_for_driver_ready_event, get_device_id, ) @@ -2854,26 +2853,18 @@ def _handle_device_added(device: dr.DeviceEntry) -> None: connection.send_result(msg[ID], device.id) async_cleanup() - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - - wait_driver_ready = asyncio.Event() - msg[DATA_UNSUBSCRIBE] = unsubs = [ async_dispatcher_connect( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added ), - driver.once("driver ready", set_driver_ready), ] + wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver) + await driver.async_hard_reset() with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() - + await wait_for_driver_ready() # When resetting the controller, the controller home id is also changed. # The controller state in the client is stale after resetting the controller, # so get the new home id with a new client using the helper function. @@ -2886,14 +2877,14 @@ def set_driver_ready(event: dict) -> None: # The stale unique id needs to be handled by a repair flow, # after the config entry has been reloaded. LOGGER.error( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller reset" ) else: hass.config_entries.async_update_entry( entry, unique_id=str(version_info.home_id) ) - await hass.config_entries.async_reload(entry.entry_id) + hass.config_entries.async_schedule_reload(entry.entry_id) @websocket_api.websocket_command( @@ -3100,27 +3091,19 @@ def forward_progress(event: dict) -> None: ) ) - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - - wait_driver_ready = asyncio.Event() - # Set up subscription for progress events connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), - driver.once("driver ready", set_driver_ready), ] + wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver) + await controller.async_restore_nvm_base64(msg["data"], {"preserveRoutes": False}) with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() - + await wait_for_driver_ready() # When restoring the NVM to the controller, the controller home id is also changed. # The controller state in the client is stale after restoring the NVM, # so get the new home id with a new client using the helper function. @@ -3133,14 +3116,13 @@ def set_driver_ready(event: dict) -> None: # The stale unique id needs to be handled by a repair flow, # after the config entry has been reloaded. LOGGER.error( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller NVM restore" ) else: hass.config_entries.async_update_entry( entry, unique_id=str(version_info.home_id) ) - await hass.config_entries.async_reload(entry.entry_id) connection.send_message( @@ -3152,3 +3134,4 @@ def set_driver_ready(event: dict) -> None: ) ) connection.send_result(msg[ID]) + async_cleanup() diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index d98dcf3dac83f..308e6c9cc1af4 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -62,9 +62,12 @@ CONF_USB_PATH, CONF_USE_ADDON, DOMAIN, - DRIVER_READY_TIMEOUT, ) -from .helpers import CannotConnect, async_get_version_info +from .helpers import ( + CannotConnect, + async_get_version_info, + async_wait_for_driver_ready_event, +) from .models import ZwaveJSConfigEntry _LOGGER = logging.getLogger(__name__) @@ -1396,19 +1399,15 @@ def forward_progress(event: dict) -> None: event["bytesWritten"] / event["total"] * 0.5 + 0.5 ) - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - driver = self._get_driver() controller = driver.controller - wait_driver_ready = asyncio.Event() unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), - driver.once("driver ready", set_driver_ready), ] + + wait_for_driver_ready = async_wait_for_driver_ready_event(config_entry, driver) + try: await controller.async_restore_nvm( self.backup_data, {"preserveRoutes": False} @@ -1417,8 +1416,7 @@ def set_driver_ready(event: dict) -> None: raise AbortFlow(f"Failed to restore network: {err}") from err else: with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() + await wait_for_driver_ready() try: version_info = await async_get_version_info( self.hass, config_entry.data[CONF_URL] @@ -1435,10 +1433,10 @@ def set_driver_ready(event: dict) -> None: self.hass.config_entries.async_update_entry( config_entry, unique_id=str(version_info.home_id) ) - await self.hass.config_entries.async_reload(config_entry.entry_id) - # Reload the config entry two times to clean up - # the stale device entry. + # The config entry will be also be reloaded when the driver is ready, + # by the listener in the package module, + # and two reloads are needed to clean up the stale controller device entry. # Since both the old and the new controller have the same node id, # but different hardware identifiers, the integration # will create a new device for the new controller, on the first reload, diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 6dc76ebd05d4e..0ccf51539d6a7 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -201,7 +201,3 @@ WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE, WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION, } - -# Other constants - -DRIVER_READY_TIMEOUT = 60 diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 5694be5482bc5..17f4909662cdd 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import astuple, dataclass import logging from typing import Any, cast @@ -56,6 +56,7 @@ ) from .models import ZwaveJSConfigEntry +DRIVER_READY_EVENT_TIMEOUT = 60 SERVER_VERSION_TIMEOUT = 10 @@ -588,5 +589,57 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio return version_info +@callback +def async_wait_for_driver_ready_event( + config_entry: ZwaveJSConfigEntry, + driver: Driver, +) -> Callable[[], Coroutine[Any, Any, None]]: + """Wait for the driver ready event and the config entry reload. + + When the driver ready event is received + the config entry will be reloaded by the integration. + This function helps wait for that to happen + before proceeding with further actions. + + If the config entry is reloaded for another reason, + this function will not wait for it to be reloaded again. + + Raises TimeoutError if the driver ready event and reload + is not received within the specified timeout. + """ + driver_ready_event_received = asyncio.Event() + config_entry_reloaded = asyncio.Event() + unsubscribers: list[Callable[[], None]] = [] + + @callback + def driver_ready_received(event: dict) -> None: + """Receive the driver ready event.""" + driver_ready_event_received.set() + + unsubscribers.append(driver.once("driver ready", driver_ready_received)) + + @callback + def on_config_entry_state_change() -> None: + """Check config entry was loaded after driver ready event.""" + if config_entry.state is ConfigEntryState.LOADED: + config_entry_reloaded.set() + + unsubscribers.append( + config_entry.async_on_state_change(on_config_entry_state_change) + ) + + async def wait_for_events() -> None: + try: + async with asyncio.timeout(DRIVER_READY_EVENT_TIMEOUT): + await asyncio.gather( + driver_ready_event_received.wait(), config_entry_reloaded.wait() + ) + finally: + for unsubscribe in unsubscribers: + unsubscribe() + + return wait_for_events + + class CannotConnect(HomeAssistantError): """Indicate connection error.""" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 6359f4bf5e775..0b83d08072c18 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1,5 +1,6 @@ """Test the Z-Wave JS Websocket API.""" +import asyncio from copy import deepcopy from http import HTTPStatus from io import BytesIO @@ -5109,17 +5110,12 @@ async def test_hard_reset_controller( ws_client = await hass_ws_client(hass) assert entry.unique_id == "3245146787" - async def async_send_command_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" + async def mock_driver_hard_reset() -> None: client.driver.emit( "driver ready", {"event": "driver ready", "source": "driver"} ) - return {} - client.async_send_command.side_effect = async_send_command_driver_ready + client.driver.async_hard_reset = AsyncMock(side_effect=mock_driver_hard_reset) await ws_client.send_json_auto_id( { @@ -5128,6 +5124,7 @@ async def async_send_command_driver_ready( } ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5135,16 +5132,10 @@ async def async_send_command_driver_ready( assert device is not None assert msg["result"] == device.id assert msg["success"] - - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 - ) + assert client.driver.async_hard_reset.call_count == 1 assert entry.unique_id == "1234" - client.async_send_command.reset_mock() + client.driver.async_hard_reset.reset_mock() # Test client connect error when getting the server version. @@ -5158,6 +5149,7 @@ async def async_send_command_driver_ready( ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5165,33 +5157,24 @@ async def async_send_command_driver_ready( assert device is not None assert msg["result"] == device.id assert msg["success"] - - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 - ) + assert client.driver.async_hard_reset.call_count == 1 assert ( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller reset" ) in caplog.text - client.async_send_command.reset_mock() + client.driver.async_hard_reset.reset_mock() + get_server_version.side_effect = None # Test sending command with driver not ready and timeout. - async def async_send_command_no_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" - return {} + async def mock_driver_hard_reset_no_driver_ready() -> None: + pass - client.async_send_command.side_effect = async_send_command_no_driver_ready + client.driver.async_hard_reset.side_effect = mock_driver_hard_reset_no_driver_ready with patch( - "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT", new=0, ): await ws_client.send_json_auto_id( @@ -5201,6 +5184,7 @@ async def async_send_command_no_driver_ready( } ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5208,32 +5192,29 @@ async def async_send_command_no_driver_ready( assert device is not None assert msg["result"] == device.id assert msg["success"] + assert client.driver.async_hard_reset.call_count == 1 + + client.driver.async_hard_reset.reset_mock() - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 + # Test FailedZWaveCommand is caught + client.driver.async_hard_reset.side_effect = FailedZWaveCommand( + "failed_command", 1, "error message" ) - client.async_send_command.reset_mock() + await ws_client.send_json_auto_id( + { + TYPE: "zwave_js/hard_reset_controller", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() - # Test FailedZWaveCommand is caught - with patch( - "zwave_js_server.model.driver.Driver.async_hard_reset", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - await ws_client.send_json_auto_id( - { - TYPE: "zwave_js/hard_reset_controller", - ENTRY_ID: entry.entry_id, - } - ) - msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message" + assert client.driver.async_hard_reset.call_count == 1 - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" - assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message" + client.driver.async_hard_reset.side_effect = None # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) @@ -5578,17 +5559,24 @@ async def test_restore_nvm( # Set up mocks for the controller events controller = client.driver.controller - async def async_send_command_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" + async def mock_restore_nvm_base64( + self, base64_data: str, options: dict[str, bool] | None = None + ) -> None: + controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 150, "total": 200}, + ) + controller.data["homeId"] = 3245146787 client.driver.emit( "driver ready", {"event": "driver ready", "source": "driver"} ) - return {} - client.async_send_command.side_effect = async_send_command_driver_ready + controller.async_restore_nvm_base64 = AsyncMock(side_effect=mock_restore_nvm_base64) # Send the subscription request await ws_client.send_json_auto_id( @@ -5599,7 +5587,19 @@ async def async_send_command_driver_ready( } ) - # Verify the finished event first + # Verify the convert progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm convert progress" + assert msg["event"]["bytesRead"] == 100 + assert msg["event"]["total"] == 200 + + # Verify the restore progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 150 + assert msg["event"]["total"] == 200 + + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" assert msg["event"]["event"] == "finished" @@ -5609,53 +5609,18 @@ async def async_send_command_driver_ready( assert msg["type"] == "result" assert msg["success"] is True - # Simulate progress events - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 25, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 25 - assert msg["event"]["total"] == 100 - - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 50, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 50 - assert msg["event"]["total"] == 100 - await hass.async_block_till_done() # Verify the restore was called # The first call is the relevant one for nvm restore. - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) assert entry.unique_id == "1234" - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() # Test client connect error when getting the server version. @@ -5670,7 +5635,19 @@ async def async_send_command_driver_ready( } ) - # Verify the finished event first + # Verify the convert progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm convert progress" + assert msg["event"]["bytesRead"] == 100 + assert msg["event"]["total"] == 200 + + # Verify the restore progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 150 + assert msg["event"]["total"] == 200 + + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" assert msg["event"]["event"] == "finished" @@ -5680,47 +5657,46 @@ async def async_send_command_driver_ready( assert msg["type"] == "result" assert msg["success"] is True - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + await hass.async_block_till_done() + + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) assert ( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller NVM restore" ) in caplog.text - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() + get_server_version.side_effect = None - # Test sending command with driver not ready and timeout. + # Test sending command without driver ready event causing timeout. - async def async_send_command_no_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" - return {} + async def mock_restore_nvm_without_driver_ready( + data: bytes, options: dict[str, bool] | None = None + ): + controller.data["homeId"] = 3245146787 - client.async_send_command.side_effect = async_send_command_no_driver_ready + controller.async_restore_nvm_base64.side_effect = ( + mock_restore_nvm_without_driver_ready + ) with patch( - "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT", new=0, ): # Send the subscription request await ws_client.send_json_auto_id( { "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, + "entry_id": entry.entry_id, "data": "dGVzdA==", # base64 encoded "test" } ) - # Verify the finished event first + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" @@ -5734,37 +5710,41 @@ async def async_send_command_no_driver_ready( await hass.async_block_till_done() # Verify the restore was called - # The first call is the relevant one for nvm restore. - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() # Test restore failure - with patch( - f"{CONTROLLER_PATCH_PREFIX}.async_restore_nvm_base64", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - # Send the subscription request - await ws_client.send_json_auto_id( - { - "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, - "data": "dGVzdA==", # base64 encoded "test" - } - ) + controller.async_restore_nvm_base64.side_effect = FailedZWaveCommand( + "failed_command", 1, "error message" + ) - # Verify error response - msg = await ws_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" + # Send the subscription request + await ws_client.send_json_auto_id( + { + "type": "zwave_js/restore_nvm", + "entry_id": entry.entry_id, + "data": "dGVzdA==", # base64 encoded "test" + } + ) + + # Verify error response + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + + await hass.async_block_till_done() + + # Verify the restore was called + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, + ) # Test entry_id not found await ws_client.send_json_auto_id( @@ -5779,13 +5759,13 @@ async def async_send_command_no_driver_ready( assert msg["error"]["code"] == "not_found" # Test config entry not loaded - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json_auto_id( { "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, + "entry_id": entry.entry_id, "data": "dGVzdA==", # base64 encoded "test" } ) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 15ec6959caf43..52b840fb69079 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1101,7 +1101,7 @@ async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): assert restart_addon.call_args == call("core_zwave_js") with patch( - ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + ("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"), new=0, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -1111,7 +1111,7 @@ async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 4 + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3897,7 +3897,7 @@ async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): assert restart_addon.call_args == call("core_zwave_js") with patch( - ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + ("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"), new=0, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -3907,7 +3907,7 @@ async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 4 + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 4decb061ad0fd..3c39868ff9314 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -2262,3 +2262,38 @@ async def test_entity_available_when_node_dead( state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) assert state assert state.state != STATE_UNAVAILABLE + + +async def test_driver_ready_event( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test receiving a driver ready event.""" + config_entry = integration + assert config_entry.state is ConfigEntryState.LOADED + + config_entry_state_changes: list[ConfigEntryState] = [] + + def on_config_entry_state_change() -> None: + """Collect config entry state changes.""" + config_entry_state_changes.append(config_entry.state) + + config_entry.async_on_state_change(on_config_entry_state_change) + + driver_ready = Event( + type="driver ready", + data={ + "source": "driver", + "event": "driver ready", + }, + ) + + client.driver.receive_event(driver_ready) + await hass.async_block_till_done() + + assert len(config_entry_state_changes) == 4 + assert config_entry_state_changes[0] == ConfigEntryState.UNLOAD_IN_PROGRESS + assert config_entry_state_changes[1] == ConfigEntryState.NOT_LOADED + assert config_entry_state_changes[2] == ConfigEntryState.SETUP_IN_PROGRESS + assert config_entry_state_changes[3] == ConfigEntryState.LOADED From 47a7ed4084a8fc2707410dd648a8c1cfe062a340 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 3 Aug 2025 20:07:01 +0200 Subject: [PATCH 049/247] Bump `imgw_pib` to version 1.5.2 (#149892) --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 62a4f41ba1fcb..e65ccf35fb58f 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["imgw_pib==1.5.1"] + "requirements": ["imgw_pib==1.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index fc2860b3bdda5..519fbdd421e55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1240,7 +1240,7 @@ ihcsdk==2.8.5 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.1 +imgw_pib==1.5.2 # homeassistant.components.incomfort incomfort-client==0.6.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e7194ff011b4..c428408da01a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1074,7 +1074,7 @@ igloohome-api==0.1.1 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.1 +imgw_pib==1.5.2 # homeassistant.components.incomfort incomfort-client==0.6.9 From 027052440dfa94400300ecc67cb90e0250b5fb61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 15:02:30 -1000 Subject: [PATCH 050/247] Bump yalexs-ble to 3.1.2 (#149917) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 2368c848eea81..e7af7d84942cc 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 5b45628ee64c4..aa68009ac7296 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 7a02afbc5d75a..b1fad926f1d8f 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==3.1.0"] + "requirements": ["yalexs-ble==3.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 519fbdd421e55..9fb41502185ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3163,7 +3163,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==3.1.0 +yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c428408da01a1..5a9fb2f13f3a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2613,7 +2613,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==3.1.0 +yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale From 5e8cd19cc31f2973f677820666df71857ef61f8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:42:38 -1000 Subject: [PATCH 051/247] Bump aiodiscover to 2.7.1 (#149920) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index ea2a4f4f820af..599e5ecae5ba3 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -16,7 +16,7 @@ "quality_scale": "internal", "requirements": [ "aiodhcpwatcher==1.2.0", - "aiodiscover==2.7.0", + "aiodiscover==2.7.1", "cached-ipaddress==0.10.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cd0fc31b00875..579d48d50f04d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ # Automatically generated by gen_requirements_all.py, do not edit aiodhcpwatcher==1.2.0 -aiodiscover==2.7.0 +aiodiscover==2.7.1 aiodns==3.5.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9fb41502185ef..e153482b53687 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aiocomelit==0.12.3 aiodhcpwatcher==1.2.0 # homeassistant.components.dhcp -aiodiscover==2.7.0 +aiodiscover==2.7.1 # homeassistant.components.dnsip aiodns==3.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a9fb2f13f3a3..54bb45a636374 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ aiocomelit==0.12.3 aiodhcpwatcher==1.2.0 # homeassistant.components.dhcp -aiodiscover==2.7.0 +aiodiscover==2.7.1 # homeassistant.components.dnsip aiodns==3.5.0 From b789c1121762217b1c623f76d932af4f81ed2142 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:42:11 -1000 Subject: [PATCH 052/247] Bump dbus-fast to 2.44.3 (#149921) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3b1e6e70ff6ab..cd6aae9125921 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,7 +20,7 @@ "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", - "dbus-fast==2.44.2", + "dbus-fast==2.44.3", "habluetooth==4.0.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 579d48d50f04d..a039d985ea06a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 cryptography==45.0.3 -dbus-fast==2.44.2 +dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 diff --git a/requirements_all.txt b/requirements_all.txt index e153482b53687..20d52b83c90ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ datadog==0.52.0 datapoint==0.12.1 # homeassistant.components.bluetooth -dbus-fast==2.44.2 +dbus-fast==2.44.3 # homeassistant.components.debugpy debugpy==1.8.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54bb45a636374..a1f5885c5f75f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ datadog==0.52.0 datapoint==0.12.1 # homeassistant.components.bluetooth -dbus-fast==2.44.2 +dbus-fast==2.44.3 # homeassistant.components.debugpy debugpy==1.8.14 From 9ef7c6c99a00f949602561dde0b62913c52559d0 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 4 Aug 2025 04:17:25 -0400 Subject: [PATCH 053/247] Bump ZHA to 0.0.65 (#149922) --- 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 ec08c4f5d9ddd..facde4ead3a1d 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.64"], + "requirements": ["zha==0.0.65"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 20d52b83c90ae..d178e5829dfdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3203,7 +3203,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.64 +zha==0.0.65 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1f5885c5f75f..ae84f9dc8fa87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2647,7 +2647,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.64 +zha==0.0.65 # homeassistant.components.zwave_js zwave-js-server-python==0.67.0 From 2b7a434677837a76af681d308db69a6ad8182cec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 4 Aug 2025 10:37:10 +0200 Subject: [PATCH 054/247] Bump version to 2025.8.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 596a99afb9272..85210a5456af1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index e454bdde6ab11..523cb7ed28936 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0b1" +version = "2025.8.0b2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From e48820b2c16ea8e00b5f98f69e4f4f1e57f15b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Tue, 5 Aug 2025 11:19:03 +0200 Subject: [PATCH 055/247] Matter pump setpoint CurrentLevel limit (#149689) --- homeassistant/components/matter/number.py | 4 +++- tests/components/matter/fixtures/nodes/pump.json | 2 +- tests/components/matter/snapshots/test_number.ambr | 2 +- tests/components/matter/test_number.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/matter/number.py b/homeassistant/components/matter/number.py index 4456496d52ec9..d2184891dc1be 100644 --- a/homeassistant/components/matter/number.py +++ b/homeassistant/components/matter/number.py @@ -285,7 +285,9 @@ def _update_from_device(self) -> None: native_min_value=0.5, native_step=0.5, device_to_ha=( - lambda x: None if x is None else x / 2 # Matter range (1-200) + lambda x: None + if x is None + else min(x, 200) / 2 # Matter range (1-200, capped at 200) ), ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0% mode=NumberMode.SLIDER, diff --git a/tests/components/matter/fixtures/nodes/pump.json b/tests/components/matter/fixtures/nodes/pump.json index e4afc0b4f33d8..6d74b3d1b89c1 100644 --- a/tests/components/matter/fixtures/nodes/pump.json +++ b/tests/components/matter/fixtures/nodes/pump.json @@ -203,7 +203,7 @@ "1/6/65528": [], "1/6/65529": [0, 1, 2], "1/6/65531": [0, 65532, 65533, 65528, 65529, 65531], - "1/8/0": 254, + "1/8/0": 200, "1/8/15": 0, "1/8/17": 0, "1/8/65532": 0, diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index f7f467b4ed0a3..24a92799082fb 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -2189,7 +2189,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '127.0', + 'state': '100.0', }) # --- # name: test_numbers[silabs_laundrywasher][number.laundrywasher_temperature_setpoint-entry] diff --git a/tests/components/matter/test_number.py b/tests/components/matter/test_number.py index b59e6848f6317..d35a889a436db 100644 --- a/tests/components/matter/test_number.py +++ b/tests/components/matter/test_number.py @@ -172,7 +172,7 @@ async def test_pump_level( # CurrentLevel on LevelControl cluster state = hass.states.get("number.mock_pump_setpoint") assert state - assert state.state == "127.0" + assert state.state == "100.0" set_node_attribute(matter_node, 1, 8, 0, 100) await trigger_subscription_callback(hass, matter_client) From 49c23de2d2137f179e5e5effd39d5d2e4717837f Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:24:51 +0200 Subject: [PATCH 056/247] Update sensor icons in Volvo integration (#149811) --- homeassistant/components/volvo/icons.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/volvo/icons.json b/homeassistant/components/volvo/icons.json index 8e2897c66ad88..61f67bcfe048a 100644 --- a/homeassistant/components/volvo/icons.json +++ b/homeassistant/components/volvo/icons.json @@ -20,7 +20,11 @@ "default": "mdi:gas-station" }, "charger_connection_status": { - "default": "mdi:ev-plug-ccs2" + "default": "mdi:power-plug-off", + "state": { + "connected": "mdi:power-plug", + "fault": "mdi:flash-alert" + } }, "charging_power": { "default": "mdi:gauge-empty", @@ -44,22 +48,22 @@ } }, "distance_to_empty_battery": { - "default": "mdi:gauge-empty" + "default": "mdi:battery-outline" }, "distance_to_empty_tank": { "default": "mdi:gauge-empty" }, "distance_to_service": { - "default": "mdi:wrench-clock" + "default": "mdi:wrench-check" }, "engine_time_to_service": { - "default": "mdi:wrench-clock" + "default": "mdi:wrench-cog" }, "estimated_charging_time": { "default": "mdi:battery-clock" }, "fuel_amount": { - "default": "mdi:gas-station" + "default": "mdi:fuel" }, "odometer": { "default": "mdi:counter" From 636c1b7e4f16ca0f5822deb540cf3f40d9350623 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 4 Aug 2025 11:40:11 -0400 Subject: [PATCH 057/247] Add translation strings for unsupported OS version (#149837) --- homeassistant/components/hassio/strings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 6d67b4b79c05c..1272b062c8bb7 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -225,6 +225,10 @@ "unsupported_virtualization_image": { "title": "Unsupported system - Incorrect OS image for virtualization", "description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this." + }, + "unsupported_os_version": { + "title": "Unsupported system - Home Assistant OS version", + "description": "System is unsupported because the Home Assistant OS version in use is not supported. Use the link to learn more and how to fix this." } }, "entity": { From 90fc7d314b55cdfc8173a90a4d60c9569b225e5a Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 4 Aug 2025 22:35:47 +0200 Subject: [PATCH 058/247] Bump python-airos to 0.2.4 (#149885) --- homeassistant/components/airos/config_flow.py | 18 +++--- homeassistant/components/airos/coordinator.py | 18 +++--- homeassistant/components/airos/manifest.json | 2 +- homeassistant/components/airos/sensor.py | 7 --- homeassistant/components/airos/strings.json | 7 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../airos/snapshots/test_sensor.ambr | 58 ------------------- tests/components/airos/test_config_flow.py | 12 ++-- tests/components/airos/test_sensor.py | 12 ++-- 10 files changed, 35 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/airos/config_flow.py b/homeassistant/components/airos/config_flow.py index 287f54101c809..8df93c7b2c433 100644 --- a/homeassistant/components/airos/config_flow.py +++ b/homeassistant/components/airos/config_flow.py @@ -6,11 +6,11 @@ from typing import Any from airos.exceptions import ( - ConnectionAuthenticationError, - ConnectionSetupError, - DataMissingError, - DeviceConnectionError, - KeyDataMissingError, + AirOSConnectionAuthenticationError, + AirOSConnectionSetupError, + AirOSDataMissingError, + AirOSDeviceConnectionError, + AirOSKeyDataMissingError, ) import voluptuous as vol @@ -59,13 +59,13 @@ async def async_step_user( airos_data = await airos_device.status() except ( - ConnectionSetupError, - DeviceConnectionError, + AirOSConnectionSetupError, + AirOSDeviceConnectionError, ): errors["base"] = "cannot_connect" - except (ConnectionAuthenticationError, DataMissingError): + except (AirOSConnectionAuthenticationError, AirOSDataMissingError): errors["base"] = "invalid_auth" - except KeyDataMissingError: + except AirOSKeyDataMissingError: errors["base"] = "key_data_missing" except Exception: _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/airos/coordinator.py b/homeassistant/components/airos/coordinator.py index 3f0f1a1238034..2fe675ee76a61 100644 --- a/homeassistant/components/airos/coordinator.py +++ b/homeassistant/components/airos/coordinator.py @@ -6,10 +6,10 @@ from airos.airos8 import AirOS, AirOSData from airos.exceptions import ( - ConnectionAuthenticationError, - ConnectionSetupError, - DataMissingError, - DeviceConnectionError, + AirOSConnectionAuthenticationError, + AirOSConnectionSetupError, + AirOSDataMissingError, + AirOSDeviceConnectionError, ) from homeassistant.config_entries import ConfigEntry @@ -47,18 +47,22 @@ async def _async_update_data(self) -> AirOSData: try: await self.airos_device.login() return await self.airos_device.status() - except (ConnectionAuthenticationError,) as err: + except (AirOSConnectionAuthenticationError,) as err: _LOGGER.exception("Error authenticating with airOS device") raise ConfigEntryError( translation_domain=DOMAIN, translation_key="invalid_auth" ) from err - except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err: + except ( + AirOSConnectionSetupError, + AirOSDeviceConnectionError, + TimeoutError, + ) as err: _LOGGER.error("Error connecting to airOS device: %s", err) raise UpdateFailed( translation_domain=DOMAIN, translation_key="cannot_connect", ) from err - except (DataMissingError,) as err: + except (AirOSDataMissingError,) as err: _LOGGER.error("Expected data not returned by airOS device: %s", err) raise UpdateFailed( translation_domain=DOMAIN, diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index cb6119a6fa99c..758902bbaa226 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.2.1"] + "requirements": ["airos==0.2.4"] } diff --git a/homeassistant/components/airos/sensor.py b/homeassistant/components/airos/sensor.py index 690bf21fc8ea2..4567261ba4d7e 100644 --- a/homeassistant/components/airos/sensor.py +++ b/homeassistant/components/airos/sensor.py @@ -69,13 +69,6 @@ class AirOSSensorEntityDescription(SensorEntityDescription): translation_key="wireless_essid", value_fn=lambda data: data.wireless.essid, ), - AirOSSensorEntityDescription( - key="wireless_mode", - translation_key="wireless_mode", - device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(), - options=WIRELESS_MODE_OPTIONS, - ), AirOSSensorEntityDescription( key="wireless_antenna_gain", translation_key="wireless_antenna_gain", diff --git a/homeassistant/components/airos/strings.json b/homeassistant/components/airos/strings.json index 6823ba8520b0d..ff013862ee56d 100644 --- a/homeassistant/components/airos/strings.json +++ b/homeassistant/components/airos/strings.json @@ -43,13 +43,6 @@ "wireless_essid": { "name": "Wireless SSID" }, - "wireless_mode": { - "name": "Wireless mode", - "state": { - "ap_ptp": "Access point", - "sta_ptp": "Station" - } - }, "wireless_antenna_gain": { "name": "Antenna gain" }, diff --git a/requirements_all.txt b/requirements_all.txt index d178e5829dfdb..f3e30047f2bc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.1 +airos==0.2.4 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae84f9dc8fa87..fac72b27d97ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.1 +airos==0.2.4 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/tests/components/airos/snapshots/test_sensor.ambr b/tests/components/airos/snapshots/test_sensor.ambr index a92d2dc35a214..e414d35beb228 100644 --- a/tests/components/airos/snapshots/test_sensor.ambr +++ b/tests/components/airos/snapshots/test_sensor.ambr @@ -439,64 +439,6 @@ 'state': '5500', }) # --- -# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'ap_ptp', - 'sta_ptp', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wireless mode', - 'platform': 'airos', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'wireless_mode', - 'unique_id': '01:23:45:67:89:AB_wireless_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'NanoStation 5AC ap name Wireless mode', - 'options': list([ - 'ap_ptp', - 'sta_ptp', - ]), - }), - 'context': , - 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'ap_ptp', - }) -# --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_ssid-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/airos/test_config_flow.py b/tests/components/airos/test_config_flow.py index 9d2a637673267..212c80dfc2bf8 100644 --- a/tests/components/airos/test_config_flow.py +++ b/tests/components/airos/test_config_flow.py @@ -4,9 +4,9 @@ from unittest.mock import AsyncMock from airos.exceptions import ( - ConnectionAuthenticationError, - DeviceConnectionError, - KeyDataMissingError, + AirOSConnectionAuthenticationError, + AirOSDeviceConnectionError, + AirOSKeyDataMissingError, ) import pytest @@ -78,9 +78,9 @@ async def test_form_duplicate_entry( @pytest.mark.parametrize( ("exception", "error"), [ - (ConnectionAuthenticationError, "invalid_auth"), - (DeviceConnectionError, "cannot_connect"), - (KeyDataMissingError, "key_data_missing"), + (AirOSConnectionAuthenticationError, "invalid_auth"), + (AirOSDeviceConnectionError, "cannot_connect"), + (AirOSKeyDataMissingError, "key_data_missing"), (Exception, "unknown"), ], ) diff --git a/tests/components/airos/test_sensor.py b/tests/components/airos/test_sensor.py index 561741b1a2bfe..c9e675e798714 100644 --- a/tests/components/airos/test_sensor.py +++ b/tests/components/airos/test_sensor.py @@ -4,9 +4,9 @@ from unittest.mock import AsyncMock from airos.exceptions import ( - ConnectionAuthenticationError, - DataMissingError, - DeviceConnectionError, + AirOSConnectionAuthenticationError, + AirOSDataMissingError, + AirOSDeviceConnectionError, ) from freezegun.api import FrozenDateTimeFactory import pytest @@ -39,10 +39,10 @@ async def test_all_entities( @pytest.mark.parametrize( ("exception"), [ - ConnectionAuthenticationError, + AirOSConnectionAuthenticationError, TimeoutError, - DeviceConnectionError, - DataMissingError, + AirOSDeviceConnectionError, + AirOSDataMissingError, ], ) async def test_sensor_update_exception_handling( From 0dac635478910a66c207d35a9893ae7a5320250d Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Sun, 3 Aug 2025 13:18:13 +0100 Subject: [PATCH 059/247] Bump aiomealie to 0.10.1 (#149890) --- homeassistant/components/mealie/manifest.json | 2 +- homeassistant/components/mealie/todo.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/mealie/fixtures/get_recipe.json | 4 ---- .../mealie/fixtures/get_shopping_items.json | 2 -- .../mealie/snapshots/test_diagnostics.ambr | 12 ++++++------ tests/components/mealie/snapshots/test_services.ambr | 8 ++++---- tests/components/mealie/test_todo.py | 2 -- 9 files changed, 16 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index 804011b3d9a43..a744b9e6ced29 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["aiomealie==0.10.0"] + "requirements": ["aiomealie==0.10.1"] } diff --git a/homeassistant/components/mealie/todo.py b/homeassistant/components/mealie/todo.py index d42c90339225b..e31af281783d7 100644 --- a/homeassistant/components/mealie/todo.py +++ b/homeassistant/components/mealie/todo.py @@ -174,7 +174,8 @@ async def async_update_todo_item(self, item: TodoItem) -> None: if list_item.display.strip() != stripped_item_summary: update_shopping_item.note = stripped_item_summary update_shopping_item.position = position - update_shopping_item.is_food = False + if update_shopping_item.is_food is not None: + update_shopping_item.is_food = False update_shopping_item.food_id = None update_shopping_item.quantity = 0.0 update_shopping_item.checked = item.status == TodoItemStatus.COMPLETED @@ -249,7 +250,7 @@ async def async_move_todo_item( mutate_shopping_item.note = item.note mutate_shopping_item.checked = item.checked - if item.is_food: + if item.is_food or item.food_id: mutate_shopping_item.food_id = item.food_id mutate_shopping_item.unit_id = item.unit_id diff --git a/requirements_all.txt b/requirements_all.txt index f3e30047f2bc5..9dbd08a658cff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -310,7 +310,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.10.0 +aiomealie==0.10.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fac72b27d97ad..3981e282fa86d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -292,7 +292,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.10.0 +aiomealie==0.10.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/tests/components/mealie/fixtures/get_recipe.json b/tests/components/mealie/fixtures/get_recipe.json index a5ccd1876e5ba..7e42986ebdc33 100644 --- a/tests/components/mealie/fixtures/get_recipe.json +++ b/tests/components/mealie/fixtures/get_recipe.json @@ -63,8 +63,6 @@ "unit": null, "food": null, "note": "130g dark couverture chocolate (min. 55% cocoa content)", - "isFood": true, - "disableAmount": false, "display": "1 130g dark couverture chocolate (min. 55% cocoa content)", "title": null, "originalText": null, @@ -87,8 +85,6 @@ "unit": null, "food": null, "note": "150g softened butter", - "isFood": true, - "disableAmount": false, "display": "1 150g softened butter", "title": null, "originalText": null, diff --git a/tests/components/mealie/fixtures/get_shopping_items.json b/tests/components/mealie/fixtures/get_shopping_items.json index 1016440816bac..81db48f2e1ab7 100644 --- a/tests/components/mealie/fixtures/get_shopping_items.json +++ b/tests/components/mealie/fixtures/get_shopping_items.json @@ -9,8 +9,6 @@ "unit": null, "food": null, "note": "Apples", - "isFood": false, - "disableAmount": true, "display": "2 Apples", "shoppingListId": "9ce096fe-ded2-4077-877d-78ba450ab13e", "checked": false, diff --git a/tests/components/mealie/snapshots/test_diagnostics.ambr b/tests/components/mealie/snapshots/test_diagnostics.ambr index a694c72fcf696..c4d649fcec674 100644 --- a/tests/components/mealie/snapshots/test_diagnostics.ambr +++ b/tests/components/mealie/snapshots/test_diagnostics.ambr @@ -383,10 +383,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', @@ -433,10 +433,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', @@ -483,10 +483,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 257d685d8dcca..a1cb758098e57 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -1247,7 +1247,7 @@ 'image': 'SuPW', 'ingredients': list([ dict({ - 'is_food': True, + 'is_food': None, 'note': '130g dark couverture chocolate (min. 55% cocoa content)', 'quantity': 1.0, 'reference_id': 'a3adfe78-d157-44d8-98be-9c133e45bb4e', @@ -1261,7 +1261,7 @@ 'unit': None, }), dict({ - 'is_food': True, + 'is_food': None, 'note': '150g softened butter', 'quantity': 1.0, 'reference_id': 'f6ce06bf-8b02-43e6-8316-0dc3fb0da0fc', @@ -1763,7 +1763,7 @@ 'image': 'SuPW', 'ingredients': list([ dict({ - 'is_food': True, + 'is_food': None, 'note': '130g dark couverture chocolate (min. 55% cocoa content)', 'quantity': 1.0, 'reference_id': 'a3adfe78-d157-44d8-98be-9c133e45bb4e', @@ -1777,7 +1777,7 @@ 'unit': None, }), dict({ - 'is_food': True, + 'is_food': None, 'note': '150g softened butter', 'quantity': 1.0, 'reference_id': 'f6ce06bf-8b02-43e6-8316-0dc3fb0da0fc', diff --git a/tests/components/mealie/test_todo.py b/tests/components/mealie/test_todo.py index d156ef3a0f1b8..0f001cacacd10 100644 --- a/tests/components/mealie/test_todo.py +++ b/tests/components/mealie/test_todo.py @@ -221,8 +221,6 @@ async def test_moving_todo_item( display=None, checked=False, position=1, - is_food=False, - disable_amount=None, quantity=2.0, label_id=None, food_id=None, From 82d153a24096925cbc7eb66b566826efe0c47c2f Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sun, 3 Aug 2025 20:05:23 +0200 Subject: [PATCH 060/247] Fix options for error sensor in Husqvarna Automower (#149901) --- .../components/husqvarna_automower/sensor.py | 7 +- .../snapshots/test_sensor.ambr | 64 +++++++++---------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 7f2921f17faf8..c5af18c63873b 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -71,10 +71,10 @@ "cutting_drive_motor_2_defect", "cutting_drive_motor_3_defect", "cutting_height_blocked", + "cutting_height_problem", "cutting_height_problem_curr", "cutting_height_problem_dir", "cutting_height_problem_drive", - "cutting_height_problem", "cutting_motor_problem", "cutting_stopped_slope_too_steep", "cutting_system_blocked", @@ -117,7 +117,6 @@ "no_accurate_position_from_satellites", "no_confirmed_position", "no_drive", - "no_error", "no_loop_signal", "no_power_in_charging_station", "no_response_from_charger", @@ -169,8 +168,8 @@ ] -ERROR_KEY_LIST = list( - dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES]) +ERROR_KEY_LIST = sorted( + set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"} ) INACTIVE_REASONS: list = [ diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 3aa3504cc2627..6628113d8c3b7 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -205,10 +205,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -219,6 +219,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -255,6 +258,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -268,6 +272,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -283,6 +288,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -300,13 +307,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'config_entry_id': , @@ -372,10 +372,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -386,6 +386,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -422,6 +425,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -435,6 +439,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -450,6 +455,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -467,13 +474,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'context': , @@ -1568,10 +1568,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -1582,6 +1582,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -1618,6 +1621,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -1631,6 +1635,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -1646,6 +1651,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -1663,13 +1670,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'config_entry_id': , @@ -1735,10 +1735,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -1749,6 +1749,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -1785,6 +1788,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -1798,6 +1802,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -1813,6 +1818,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -1830,13 +1837,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'context': , From 53769da55ed3e1d4fd94a1ab582c2c082c384aec Mon Sep 17 00:00:00 2001 From: andreimoraru Date: Mon, 4 Aug 2025 12:15:38 +0300 Subject: [PATCH 061/247] Bump yt-dlp to 2025.07.21 (#149916) Co-authored-by: Joostlek --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 20068efccef3c..db622d21f1a12 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.06.09"], + "requirements": ["yt-dlp[default]==2025.07.21"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 9dbd08a658cff..335c348e4351f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3185,7 +3185,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.06.09 +yt-dlp[default]==2025.07.21 # homeassistant.components.zabbix zabbix-utils==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3981e282fa86d..1c2663fea3e82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2632,7 +2632,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.06.09 +yt-dlp[default]==2025.07.21 # homeassistant.components.zamg zamg==0.3.6 From 79ef51fb072e2f67bf13b18552df1dfe2c52ade3 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 4 Aug 2025 19:26:14 +1000 Subject: [PATCH 062/247] Fix credit sensor when there are no vehicles in Teslemetry (#149925) --- homeassistant/components/teslemetry/models.py | 2 +- homeassistant/components/teslemetry/sensor.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index 51eed97227e14..6d12aa56470f4 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -28,7 +28,7 @@ class TeslemetryData: vehicles: list[TeslemetryVehicleData] energysites: list[TeslemetryEnergyData] scopes: list[Scope] - stream: TeslemetryStream + stream: TeslemetryStream | None @dataclass diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 1ffe073cc5cfc..34ee2d4b8e955 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -45,7 +45,7 @@ TeslemetryVehicleStreamEntity, TeslemetryWallConnectorEntity, ) -from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData +from .models import TeslemetryEnergyData, TeslemetryVehicleData PARALLEL_UPDATES = 0 @@ -1617,11 +1617,12 @@ async def async_setup_entry( if energysite.history_coordinator is not None ) - entities.append( - TeslemetryCreditBalanceSensor( - entry.unique_id or entry.entry_id, entry.runtime_data + if entry.runtime_data.stream is not None: + entities.append( + TeslemetryCreditBalanceSensor( + entry.unique_id or entry.entry_id, entry.runtime_data.stream + ) ) - ) async_add_entities(entities) @@ -1840,12 +1841,12 @@ class TeslemetryCreditBalanceSensor(RestoreSensor): _attr_state_class = SensorStateClass.MEASUREMENT _attr_suggested_display_precision = 0 - def __init__(self, uid: str, data: TeslemetryData) -> None: + def __init__(self, uid: str, stream: TeslemetryStream) -> None: """Initialize common aspects of a Teslemetry entity.""" self._attr_translation_key = "credit_balance" self._attr_unique_id = f"{uid}_credit_balance" - self.stream = data.stream + self.stream = stream async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" From 3b1bb4112950288a4f42bc2c91281e2f039055b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Mon, 4 Aug 2025 11:05:32 +0200 Subject: [PATCH 063/247] Airthings ContextVar warning (#149930) --- homeassistant/components/airthings/__init__.py | 7 ++----- homeassistant/components/airthings/coordinator.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index 175fd32006249..04c666dc5bc25 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -7,21 +7,18 @@ from airthings import Airthings -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SECRET -from .coordinator import AirthingsDataUpdateCoordinator +from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [Platform.SENSOR] SCAN_INTERVAL = timedelta(minutes=6) -type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool: """Set up Airthings from a config entry.""" @@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> async_get_clientsession(hass), ) - coordinator = AirthingsDataUpdateCoordinator(hass, airthings) + coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/airthings/coordinator.py b/homeassistant/components/airthings/coordinator.py index 6172dc0b6ef22..9e15e4a0c5d4f 100644 --- a/homeassistant/components/airthings/coordinator.py +++ b/homeassistant/components/airthings/coordinator.py @@ -5,6 +5,7 @@ from airthings import Airthings, AirthingsDevice, AirthingsError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -13,15 +14,23 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=6) +type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator] + class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]): """Coordinator for Airthings data updates.""" - def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None: + def __init__( + self, + hass: HomeAssistant, + airthings: Airthings, + config_entry: AirthingsConfigEntry, + ) -> None: """Initialize the coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_method=self._update_method, update_interval=SCAN_INTERVAL, From aa700c39822b5292fc8ca8d638bc56faa77df623 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:27:10 +0200 Subject: [PATCH 064/247] Pass config entry to hue coordinator (#149941) --- homeassistant/components/hue/v1/light.py | 2 ++ homeassistant/components/hue/v1/sensor_base.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index b725138229660..36dfdd423efff 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -163,6 +163,7 @@ async def async_setup_entry( name="light", update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update), update_interval=SCAN_INTERVAL, + config_entry=config_entry, request_refresh_debouncer=Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), @@ -197,6 +198,7 @@ async def async_setup_entry( name="group", update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update), update_interval=SCAN_INTERVAL, + config_entry=config_entry, request_refresh_debouncer=Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index 393069b0c7c80..fb8f3c572c122 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -53,6 +53,7 @@ def __init__(self, bridge): LOGGER, name="sensor", update_method=self.async_update_data, + config_entry=bridge.config_entry, update_interval=self.SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True From a2722f08c49975007125434fac6093025cd2ca33 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:20:30 +0200 Subject: [PATCH 065/247] Pass config entry to Mill coordinator (#149942) --- homeassistant/components/mill/__init__.py | 1 + homeassistant/components/mill/coordinator.py | 2 ++ tests/components/mill/test_coordinator.py | 19 ++++++++++++++----- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 246ea7789162e..ce25871209025 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -43,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: historic_data_coordinator = MillHistoricDataUpdateCoordinator( hass, + entry, mill_data_connection=mill_data_connection, ) historic_data_coordinator.async_add_listener(lambda: None) diff --git a/homeassistant/components/mill/coordinator.py b/homeassistant/components/mill/coordinator.py index a701acb8ddbf1..ea1295376ae46 100644 --- a/homeassistant/components/mill/coordinator.py +++ b/homeassistant/components/mill/coordinator.py @@ -60,6 +60,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, *, mill_data_connection: Mill, ) -> None: @@ -70,6 +71,7 @@ def __init__( hass, _LOGGER, name="MillHistoricDataUpdateCoordinator", + config_entry=config_entry, ) async def _async_update_data(self): diff --git a/tests/components/mill/test_coordinator.py b/tests/components/mill/test_coordinator.py index a2a3bd57b65c5..2e6e08016b750 100644 --- a/tests/components/mill/test_coordinator.py +++ b/tests/components/mill/test_coordinator.py @@ -11,12 +11,15 @@ from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry from tests.components.recorder.common import async_wait_recording_done async def test_mill_historic_data(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -31,7 +34,7 @@ async def test_mill_historic_data(recorder_mock: Recorder, hass: HomeAssistant) statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -96,6 +99,8 @@ async def test_mill_historic_data_no_heater( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -110,7 +115,7 @@ async def test_mill_historic_data_no_heater( statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -133,6 +138,8 @@ async def test_mill_historic_data_no_data( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -145,7 +152,7 @@ async def test_mill_historic_data_no_data( mill_data_connection.fetch_historic_energy_usage = AsyncMock(return_value=data) coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -168,7 +175,7 @@ async def test_mill_historic_data_no_data( mill_data_connection.fetch_historic_energy_usage = AsyncMock(return_value=None) coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -192,6 +199,8 @@ async def test_mill_historic_data_invalid_data( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): None, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -206,7 +215,7 @@ async def test_mill_historic_data_invalid_data( statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) From d50b9405f07a22480240e8ede854015a3bfcca30 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:21:29 +0200 Subject: [PATCH 066/247] Pass config entry to Simplisafe coordinator (#149943) --- homeassistant/components/simplisafe/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 8a75baa69c609..67bf94c61ae5e 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -573,6 +573,7 @@ async def async_websocket_disconnect_listener(_: Event) -> None: self._hass, LOGGER, name=self.entry.title, + config_entry=self.entry, update_interval=DEFAULT_SCAN_INTERVAL, update_method=self.async_update, ) From ab5aac47b24bd43f60dd663d42d8b101fee7070f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:17:27 +0200 Subject: [PATCH 067/247] Pass config entry to Kraken coordinator (#149944) --- homeassistant/components/kraken/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index c981f3fd43836..5c3158bddf233 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -135,6 +135,7 @@ async def async_setup(self) -> None: self._hass, _LOGGER, name=DOMAIN, + config_entry=self._config_entry, update_method=self.async_update, update_interval=timedelta( seconds=self._config_entry.options[CONF_SCAN_INTERVAL] From 6cb48da2f3af0b871dbd8481750ccf21131a2807 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:14:50 +0200 Subject: [PATCH 068/247] Pass config entry to Meteo France coordinator (#149945) --- homeassistant/components/meteo_france/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 20e6c02f5d405..94918ab4d4fe0 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -63,6 +63,7 @@ async def _async_update_data_alert() -> CurrentPhenomenons: hass, _LOGGER, name=f"Météo-France forecast for city {entry.title}", + config_entry=entry, update_method=_async_update_data_forecast_forecast, update_interval=SCAN_INTERVAL, ) @@ -80,6 +81,7 @@ async def _async_update_data_alert() -> CurrentPhenomenons: hass, _LOGGER, name=f"Météo-France rain for city {entry.title}", + config_entry=entry, update_method=_async_update_data_rain, update_interval=SCAN_INTERVAL_RAIN, ) @@ -103,6 +105,7 @@ async def _async_update_data_alert() -> CurrentPhenomenons: hass, _LOGGER, name=f"Météo-France alert for department {department}", + config_entry=entry, update_method=_async_update_data_alert, update_interval=SCAN_INTERVAL, ) From a5a45ce59f6c8af595dec354cdf618ec62590923 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:58:46 +0200 Subject: [PATCH 069/247] Pass config entry to Smarttub coordinator (#149946) --- homeassistant/components/smarttub/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 337959e031679..095179d618a06 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -74,6 +74,7 @@ async def async_setup_entry(self, entry: SmartTubConfigEntry) -> bool: self._hass, _LOGGER, name=DOMAIN, + config_entry=entry, update_method=self.async_update_data, update_interval=timedelta(seconds=SCAN_INTERVAL), ) From 4e3309bd228574b606ddb2563b96aa2778a27768 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:58:19 +0200 Subject: [PATCH 070/247] Pass config entry to Snoo coordinator (#149947) --- homeassistant/components/snoo/__init__.py | 2 +- homeassistant/components/snoo/coordinator.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snoo/__init__.py b/homeassistant/components/snoo/__init__.py index 54834bf58ce24..20d94be7c033e 100644 --- a/homeassistant/components/snoo/__init__.py +++ b/homeassistant/components/snoo/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SnooConfigEntry) -> bool coordinators: dict[str, SnooCoordinator] = {} tasks = [] for device in devices: - coordinators[device.serialNumber] = SnooCoordinator(hass, device, snoo) + coordinators[device.serialNumber] = SnooCoordinator(hass, entry, device, snoo) tasks.append(coordinators[device.serialNumber].setup()) await asyncio.gather(*tasks) entry.runtime_data = coordinators diff --git a/homeassistant/components/snoo/coordinator.py b/homeassistant/components/snoo/coordinator.py index bc06d20955c31..8ce0db3462125 100644 --- a/homeassistant/components/snoo/coordinator.py +++ b/homeassistant/components/snoo/coordinator.py @@ -19,11 +19,18 @@ class SnooCoordinator(DataUpdateCoordinator[SnooData]): config_entry: SnooConfigEntry - def __init__(self, hass: HomeAssistant, device: SnooDevice, snoo: Snoo) -> None: + def __init__( + self, + hass: HomeAssistant, + entry: SnooConfigEntry, + device: SnooDevice, + snoo: Snoo, + ) -> None: """Set up Snoo Coordinator.""" super().__init__( hass, name=device.name, + config_entry=entry, logger=_LOGGER, ) self.device_unique_id = device.serialNumber From dfc16d9f15af98b5dbdad1a81d0265a110867fcf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:27:51 +0200 Subject: [PATCH 071/247] Pass config entry to Broadlink coordinator (#149949) --- homeassistant/components/broadlink/updater.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 7c1644fff540c..8fdbb5054a8c7 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -64,6 +64,7 @@ def __init__(self, device: BroadlinkDevice[_ApiT]) -> None: device.hass, _LOGGER, name=f"{device.name} ({device.api.model} at {device.api.host[0]})", + config_entry=device.config, update_method=self.async_update, update_interval=self.SCAN_INTERVAL, ) From 4b0b2682279a379150f5179e17449523298defa7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Aug 2025 12:20:30 +0200 Subject: [PATCH 072/247] Fix DeviceEntry.suggested_area deprecation warning (#149951) --- homeassistant/helpers/device_registry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 72d0cf651f2b3..c7f7d4c369d2f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1221,8 +1221,6 @@ def async_update_device( # noqa: C901 ("name", name), ("name_by_user", name_by_user), ("serial_number", serial_number), - # Can be removed when suggested_area is removed from DeviceEntry - ("suggested_area", suggested_area), ("sw_version", sw_version), ("via_device_id", via_device_id), ): @@ -1230,6 +1228,11 @@ def async_update_device( # noqa: C901 new_values[attr_name] = value old_values[attr_name] = getattr(old, attr_name) + # Can be removed when suggested_area is removed from DeviceEntry + if suggested_area is not UNDEFINED and suggested_area != old._suggested_area: # noqa: SLF001 + new_values["suggested_area"] = suggested_area + old_values["suggested_area"] = old._suggested_area # noqa: SLF001 + if old.is_new: new_values["is_new"] = False From f832a2844f7b9c108b51c92b7a8ec811bf90156a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:29:27 +0200 Subject: [PATCH 073/247] Pass config entry to Unifi coordinator (#149952) --- homeassistant/components/unifi/hub/entity_loader.py | 8 +++++--- homeassistant/components/unifi/hub/hub.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/hub/entity_loader.py b/homeassistant/components/unifi/hub/entity_loader.py index 84948a92e9881..4fd3d34a51dc2 100644 --- a/homeassistant/components/unifi/hub/entity_loader.py +++ b/homeassistant/components/unifi/hub/entity_loader.py @@ -25,6 +25,7 @@ from ..entity import UnifiEntity, UnifiEntityDescription if TYPE_CHECKING: + from .. import UnifiConfigEntry from .hub import UnifiHub CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) @@ -34,7 +35,7 @@ class UnifiEntityLoader: """UniFi Network integration handling platforms for entity registration.""" - def __init__(self, hub: UnifiHub) -> None: + def __init__(self, hub: UnifiHub, config_entry: UnifiConfigEntry) -> None: """Initialize the UniFi entity loader.""" self.hub = hub self.api_updaters = ( @@ -57,15 +58,16 @@ def __init__(self, hub: UnifiHub) -> None: ) self.wireless_clients = hub.hass.data[UNIFI_WIRELESS_CLIENTS] - self._dataUpdateCoordinator = DataUpdateCoordinator( + self._data_update_coordinator = DataUpdateCoordinator( hub.hass, LOGGER, name="Unifi entity poller", + config_entry=config_entry, update_method=self._update_pollable_api_data, update_interval=POLL_INTERVAL, ) - self._update_listener = self._dataUpdateCoordinator.async_add_listener( + self._update_listener = self._data_update_coordinator.async_add_listener( update_callback=lambda: None ) diff --git a/homeassistant/components/unifi/hub/hub.py b/homeassistant/components/unifi/hub/hub.py index 6cf8825a26cd5..9ea887bdb29a7 100644 --- a/homeassistant/components/unifi/hub/hub.py +++ b/homeassistant/components/unifi/hub/hub.py @@ -39,7 +39,7 @@ def __init__( self.hass = hass self.api = api self.config = UnifiConfig.from_config_entry(config_entry) - self.entity_loader = UnifiEntityLoader(self) + self.entity_loader = UnifiEntityLoader(self, config_entry) self._entity_helper = UnifiEntityHelper(hass, api) self.websocket = UnifiWebsocket(hass, api, self.signal_reachable) From e0e4fc8afb741d57e1ee93cf47496a7d911d88b1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:08:03 +0200 Subject: [PATCH 074/247] Pass config entry to AsusWRT coordinator (#149953) --- homeassistant/components/asuswrt/router.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index a34f191b7a780..3cf8d2e863de1 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Mapping from datetime import datetime, timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from pyasuswrt import AsusWrtError @@ -40,6 +40,9 @@ SENSORS_CONNECTED_DEVICE, ) +if TYPE_CHECKING: + from . import AsusWrtConfigEntry + CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] SCAN_INTERVAL = timedelta(seconds=30) @@ -52,10 +55,13 @@ class AsusWrtSensorDataHandler: """Data handler for AsusWrt sensor.""" - def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None: + def __init__( + self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry + ) -> None: """Initialize a AsusWrt sensor data handler.""" self._hass = hass self._api = api + self._entry = entry self._connected_devices = 0 async def _get_connected_devices(self) -> dict[str, int]: @@ -91,6 +97,7 @@ async def get_coordinator( update_method=method, # Polling interval. Will only be polled if there are subscribers. update_interval=SCAN_INTERVAL if should_poll else None, + config_entry=self._entry, ) await coordinator.async_refresh() @@ -321,7 +328,9 @@ async def init_sensors_coordinator(self) -> None: if self._sensors_data_handler: return - self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api) + self._sensors_data_handler = AsusWrtSensorDataHandler( + self.hass, self._api, self._entry + ) self._sensors_data_handler.update_device_count(self._connected_devices) sensors_types = await self._api.async_get_available_sensors() From 0c0604e5bd50c627d3e6054ba7ffd7ed77ef2e1f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:02:21 +0200 Subject: [PATCH 075/247] Pass config entry to Fronius coordinator (#149954) --- homeassistant/components/fronius/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 8a3d1ebf04cec..cfbdfbcb424f7 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -106,6 +106,7 @@ async def init_devices(self) -> None: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_logger_{self.host}", + config_entry=self.config_entry, ) await self.logger_coordinator.async_config_entry_first_refresh() @@ -120,6 +121,7 @@ async def init_devices(self) -> None: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_meters_{self.host}", + config_entry=self.config_entry, ) ) @@ -129,6 +131,7 @@ async def init_devices(self) -> None: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_ohmpilot_{self.host}", + config_entry=self.config_entry, ) ) @@ -138,6 +141,7 @@ async def init_devices(self) -> None: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_power_flow_{self.host}", + config_entry=self.config_entry, ) ) @@ -147,6 +151,7 @@ async def init_devices(self) -> None: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_storages_{self.host}", + config_entry=self.config_entry, ) ) @@ -206,6 +211,7 @@ async def _init_devices_inverter(self, _now: datetime | None = None) -> None: logger=_LOGGER, name=_inverter_name, inverter_info=_inverter_info, + config_entry=self.config_entry, ) if self.config_entry.state == ConfigEntryState.LOADED: await _coordinator.async_refresh() From b163f2b855cbf8df5815ea9419f156d53cbb50fc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:49:26 +0200 Subject: [PATCH 076/247] Pass config entry to SMS coordinator (#149955) --- homeassistant/components/sms/__init__.py | 4 ++-- homeassistant/components/sms/coordinator.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 6c7c5374f7d47..78f7899a57193 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -83,8 +83,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not gateway: raise ConfigEntryNotReady(f"Cannot find device {device}") - signal_coordinator = SignalCoordinator(hass, gateway) - network_coordinator = NetworkCoordinator(hass, gateway) + signal_coordinator = SignalCoordinator(hass, entry, gateway) + network_coordinator = NetworkCoordinator(hass, entry, gateway) # Fetch initial data so we have data when entities subscribe await signal_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/sms/coordinator.py b/homeassistant/components/sms/coordinator.py index 7bc691afedf65..858fc30380534 100644 --- a/homeassistant/components/sms/coordinator.py +++ b/homeassistant/components/sms/coordinator.py @@ -16,13 +16,14 @@ class SignalCoordinator(DataUpdateCoordinator): """Signal strength coordinator.""" - def __init__(self, hass, gateway): + def __init__(self, hass, entry, gateway): """Initialize signal strength coordinator.""" super().__init__( hass, _LOGGER, name="Device signal state", update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + config_entry=entry, ) self._gateway = gateway @@ -38,13 +39,14 @@ async def _async_update_data(self): class NetworkCoordinator(DataUpdateCoordinator): """Network info coordinator.""" - def __init__(self, hass, gateway): + def __init__(self, hass, entry, gateway): """Initialize network info coordinator.""" super().__init__( hass, _LOGGER, name="Device network state", update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + config_entry=entry, ) self._gateway = gateway From 641621d184d380740bace273975f0d2f0b94fd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 4 Aug 2025 11:32:01 +0100 Subject: [PATCH 077/247] Bump hass-nabucasa from 0.110.0 to 0.110.1 (#149956) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index a819203e549f6..63eae6261d458 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.110.0"], + "requirements": ["hass-nabucasa==0.110.1"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a039d985ea06a..6ebd9a8efb795 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==4.0.1 -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250731.0 diff --git a/pyproject.toml b/pyproject.toml index 523cb7ed28936..5f99bd491d841 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.110.0", + "hass-nabucasa==0.110.1", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", diff --git a/requirements.txt b/requirements.txt index a332eb930c211..ba08a72e324ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 335c348e4351f..d4fd8f0a0c80e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ habiticalib==0.4.1 habluetooth==4.0.1 # homeassistant.components.cloud -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c2663fea3e82..94de3c485a6a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ habiticalib==0.4.1 habluetooth==4.0.1 # homeassistant.components.cloud -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 # homeassistant.components.assist_satellite # homeassistant.components.conversation From a06557ed542d15d7fe7ad41b96fa214f3278531a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:28:59 +0200 Subject: [PATCH 078/247] Pass config entry to Remote Calendar coordinator (#149958) --- homeassistant/components/remote_calendar/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/remote_calendar/coordinator.py b/homeassistant/components/remote_calendar/coordinator.py index 26876b532244a..7a7abe37b8955 100644 --- a/homeassistant/components/remote_calendar/coordinator.py +++ b/homeassistant/components/remote_calendar/coordinator.py @@ -39,6 +39,7 @@ def __init__( _LOGGER, name=f"{DOMAIN}_{config_entry.title}", update_interval=SCAN_INTERVAL, + config_entry=config_entry, always_update=True, ) self._client = get_async_client(hass) From 778fe96eb6db7a4c95942458137901ddb25d203d Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 4 Aug 2025 07:35:13 -0400 Subject: [PATCH 079/247] Fix optimistic covers (#149962) --- homeassistant/components/template/cover.py | 1 + homeassistant/components/template/entity.py | 12 +++++++++--- tests/components/template/test_cover.py | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index caac8cf5a1de5..44981fcb08fd4 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -216,6 +216,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): _entity_id_format = ENTITY_ID_FORMAT _optimistic_entity = True + _extra_optimistic_options = (CONF_POSITION,) # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. # This ensures that the __init__ on AbstractTemplateEntity is not called twice. diff --git a/homeassistant/components/template/entity.py b/homeassistant/components/template/entity.py index e9a630594d71a..03a93f50ec3c8 100644 --- a/homeassistant/components/template/entity.py +++ b/homeassistant/components/template/entity.py @@ -20,6 +20,7 @@ class AbstractTemplateEntity(Entity): _entity_id_format: str _optimistic_entity: bool = False + _extra_optimistic_options: tuple[str, ...] | None = None _template: Template | None = None def __init__( @@ -35,9 +36,14 @@ def __init__( if self._optimistic_entity: self._template = config.get(CONF_STATE) - self._attr_assumed_state = self._template is None or config.get( - CONF_OPTIMISTIC, False - ) + optimistic = self._template is None + if self._extra_optimistic_options: + optimistic = optimistic and all( + config.get(option) is None + for option in self._extra_optimistic_options + ) + + self._attr_assumed_state = optimistic or config.get(CONF_OPTIMISTIC, False) if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index dc3428330b00e..692567c7aa897 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -239,6 +239,7 @@ async def setup_position_cover( { TEST_OBJECT_ID: { **COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position_template": position_template, } }, @@ -249,6 +250,7 @@ async def setup_position_cover( count, { **NAMED_COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position": position_template, }, ) @@ -258,6 +260,7 @@ async def setup_position_cover( count, { **NAMED_COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position": position_template, }, ) @@ -565,6 +568,7 @@ async def test_template_position( position: int | None, expected: str, caplog: pytest.LogCaptureFixture, + calls: list[ServiceCall], ) -> None: """Test the position_template attribute.""" hass.states.async_set(TEST_STATE_ENTITY_ID, CoverState.OPEN) @@ -580,6 +584,19 @@ async def test_template_position( assert state.state == expected assert "ValueError" not in caplog.text + # Test to make sure optimistic is not set with only a position template. + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, "position": 10}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("current_position") == position + assert state.state == expected + @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( From 4596c1644b16a99b7452db28ddca26d67e09ab5c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 4 Aug 2025 13:36:12 +0200 Subject: [PATCH 080/247] Direct migrations with Z-Wave JS UI to docs (#149966) --- .../components/zwave_js/config_flow.py | 18 ++++++++++++++++-- homeassistant/components/zwave_js/strings.json | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 308e6c9cc1af4..6121bd0050891 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -93,6 +93,10 @@ NETWORK_TYPE_NEW = "new" NETWORK_TYPE_EXISTING = "existing" +ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = ( + "https://www.home-assistant.io/integrations/zwave_js/" + "#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui" +) def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: @@ -446,7 +450,12 @@ async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResu None, ) if not self._reconfigure_config_entry: - return self.async_abort(reason="addon_required") + return self.async_abort( + reason="addon_required", + description_placeholders={ + "zwave_js_ui_migration": ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS, + }, + ) vid = discovery_info.vid pid = discovery_info.pid @@ -890,7 +899,12 @@ async def async_step_intent_migrate( config_entry = self._reconfigure_config_entry assert config_entry is not None if not self._usb_discovery and not config_entry.data.get(CONF_USE_ADDON): - return self.async_abort(reason="addon_required") + return self.async_abort( + reason="addon_required", + description_placeholders={ + "zwave_js_ui_migration": ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS, + }, + ) try: driver = self._get_driver() diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 0288fbd713172..8ac356a40b043 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -4,7 +4,7 @@ "addon_get_discovery_info_failed": "Failed to get Z-Wave add-on discovery info.", "addon_info_failed": "Failed to get Z-Wave add-on info.", "addon_install_failed": "Failed to install the Z-Wave add-on.", - "addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor add-on. You can still use the Backup and Restore buttons to migrate your network manually.", + "addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor add-on. If you are using Z-Wave JS UI, please follow our [migration instructions]({zwave_js_ui_migration}).", "addon_set_config_failed": "Failed to set Z-Wave configuration.", "addon_start_failed": "Failed to start the Z-Wave add-on.", "addon_stop_failed": "Failed to stop the Z-Wave add-on.", From 03bd133577e69f2c67531412db1c3964975da96c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:08:03 +0200 Subject: [PATCH 081/247] Rename Tuya fixture files (#149927) --- tests/components/tuya/__init__.py | 82 ++-- ...tor_zigbee_cover.json => cl_zah67ekd.json} | 0 ...curtain_switch.json => clkg_nhyj64w2.json} | 0 ...ector.json => co2bj_yrr3eiyiacm31ski.json} | 0 ...midifier.json => cs_ka2wfrdoogpvgzfi.json} | 0 ...dry_plus.json => cs_vmxuxszzjwp5smli.json} | 0 ...purifier.json => cs_zibqa9dutqyaxym2.json} | 0 ...or_eliminator.json => cwjwq_agwu93lr.json} | 0 ...pf100.json => cwwsq_wfkzyy0evslzsmoi.json} | 0 ...ntain.json => cwysj_z3rpyvznfcch99aa.json} | 0 ...metering.json => cz_2jxesipczks0kdct.json} | 0 ...ght_bulb.json => dj_mki13ie507rlry4r.json} | 0 ..._eawcpt.json => dlq_0tnvg2xaisqdadcf.json} | 0 ...pn_wifi.json => dlq_kxdr6su0c55p7bbo.json} | 0 ...t_light.json => gyd_lgekqfxdabipm3tn.json} | 0 ...rt_valve.json => kg_gbm9ata1zrzaez4a.json} | 0 ...ower_fan.json => kj_yrzylxax1qspdgpp.json} | 0 ...ower_fan.json => ks_j9fa8ahzac8uvlfl.json} | 0 ...ditioner.json => kt_5wnlzekkstwcdsvm.json} | 0 ...rm_host.json => mal_gyitctrjj1kefxp2.json} | 0 ..._sensor.json => mcs_7jIGJAymiH8OsFFb.json} | 0 ...ntrol.json => qccdz_7bvgooyjhiua1yyq.json} | 0 ...station.json => qxj_fsea1lat3vuktbt6.json} | 0 ...l_probe.json => qxj_is2indt9nlth6esa.json} | 0 ...sensor.json => rqbj_4iqe2hsfyd86kwwc.json} | 0 ...oller.json => sfkzq_o6dagifntoafakst.json} | 0 ...q_4_443.json => tdq_cq1p0nt0a4rixnex.json} | 0 ..._air_conditioner.json => wk_aqoouq7x.json} | 0 ...ermostat.json => wk_fi6dne5tu4t1nm6j.json} | 0 ...idity.json => wsdcg_g2y6z3p3ja2qhyav.json} | 0 ...switch.json => wxkg_l8yaz4um5b3pwyvf.json} | 0 ...ported.json => ydkt_jevroj5aguwdbs2e.json} | 0 ..._meter.json => zndb_ze8faryrxr0glqnn.json} | 0 .../snapshots/test_alarm_control_panel.ambr | 4 +- .../tuya/snapshots/test_binary_sensor.ambr | 92 ++--- .../tuya/snapshots/test_climate.ambr | 12 +- .../components/tuya/snapshots/test_cover.ambr | 8 +- .../tuya/snapshots/test_diagnostics.ambr | 4 +- .../components/tuya/snapshots/test_event.ambr | 8 +- tests/components/tuya/snapshots/test_fan.ambr | 64 +-- .../tuya/snapshots/test_humidifier.ambr | 64 +-- .../components/tuya/snapshots/test_init.ambr | 2 +- .../components/tuya/snapshots/test_light.ambr | 16 +- .../tuya/snapshots/test_number.ambr | 24 +- .../tuya/snapshots/test_select.ambr | 48 +-- .../tuya/snapshots/test_sensor.ambr | 372 +++++++++--------- .../components/tuya/snapshots/test_siren.ambr | 4 +- .../tuya/snapshots/test_switch.ambr | 148 +++---- tests/components/tuya/test_binary_sensor.py | 4 +- tests/components/tuya/test_climate.py | 8 +- tests/components/tuya/test_cover.py | 10 +- tests/components/tuya/test_diagnostics.py | 4 +- tests/components/tuya/test_humidifier.py | 12 +- tests/components/tuya/test_init.py | 2 +- tests/components/tuya/test_light.py | 4 +- tests/components/tuya/test_number.py | 4 +- tests/components/tuya/test_select.py | 4 +- 57 files changed, 502 insertions(+), 502 deletions(-) rename tests/components/tuya/fixtures/{cl_am43_corded_motor_zigbee_cover.json => cl_zah67ekd.json} (100%) rename tests/components/tuya/fixtures/{clkg_curtain_switch.json => clkg_nhyj64w2.json} (100%) rename tests/components/tuya/fixtures/{co2bj_air_detector.json => co2bj_yrr3eiyiacm31ski.json} (100%) rename tests/components/tuya/fixtures/{cs_emma_dehumidifier.json => cs_ka2wfrdoogpvgzfi.json} (100%) rename tests/components/tuya/fixtures/{cs_smart_dry_plus.json => cs_vmxuxszzjwp5smli.json} (100%) rename tests/components/tuya/fixtures/{cs_arete_two_12l_dehumidifier_air_purifier.json => cs_zibqa9dutqyaxym2.json} (100%) rename tests/components/tuya/fixtures/{cwjwq_smart_odor_eliminator.json => cwjwq_agwu93lr.json} (100%) rename tests/components/tuya/fixtures/{cwwsq_cleverio_pf100.json => cwwsq_wfkzyy0evslzsmoi.json} (100%) rename tests/components/tuya/fixtures/{cwysj_pixi_smart_drinking_fountain.json => cwysj_z3rpyvznfcch99aa.json} (100%) rename tests/components/tuya/fixtures/{cz_dual_channel_metering.json => cz_2jxesipczks0kdct.json} (100%) rename tests/components/tuya/fixtures/{dj_smart_light_bulb.json => dj_mki13ie507rlry4r.json} (100%) rename tests/components/tuya/fixtures/{dlq_earu_electric_eawcpt.json => dlq_0tnvg2xaisqdadcf.json} (100%) rename tests/components/tuya/fixtures/{dlq_metering_3pn_wifi.json => dlq_kxdr6su0c55p7bbo.json} (100%) rename tests/components/tuya/fixtures/{gyd_night_light.json => gyd_lgekqfxdabipm3tn.json} (100%) rename tests/components/tuya/fixtures/{kg_smart_valve.json => kg_gbm9ata1zrzaez4a.json} (100%) rename tests/components/tuya/fixtures/{kj_bladeless_tower_fan.json => kj_yrzylxax1qspdgpp.json} (100%) rename tests/components/tuya/fixtures/{ks_tower_fan.json => ks_j9fa8ahzac8uvlfl.json} (100%) rename tests/components/tuya/fixtures/{kt_serenelife_slpac905wuk_air_conditioner.json => kt_5wnlzekkstwcdsvm.json} (100%) rename tests/components/tuya/fixtures/{mal_alarm_host.json => mal_gyitctrjj1kefxp2.json} (100%) rename tests/components/tuya/fixtures/{mcs_door_sensor.json => mcs_7jIGJAymiH8OsFFb.json} (100%) rename tests/components/tuya/fixtures/{qccdz_ac_charging_control.json => qccdz_7bvgooyjhiua1yyq.json} (100%) rename tests/components/tuya/fixtures/{qxj_weather_station.json => qxj_fsea1lat3vuktbt6.json} (100%) rename tests/components/tuya/fixtures/{qxj_temp_humidity_external_probe.json => qxj_is2indt9nlth6esa.json} (100%) rename tests/components/tuya/fixtures/{rqbj_gas_sensor.json => rqbj_4iqe2hsfyd86kwwc.json} (100%) rename tests/components/tuya/fixtures/{sfkzq_valve_controller.json => sfkzq_o6dagifntoafakst.json} (100%) rename tests/components/tuya/fixtures/{tdq_4_443.json => tdq_cq1p0nt0a4rixnex.json} (100%) rename tests/components/tuya/fixtures/{wk_air_conditioner.json => wk_aqoouq7x.json} (100%) rename tests/components/tuya/fixtures/{wk_wifi_smart_gas_boiler_thermostat.json => wk_fi6dne5tu4t1nm6j.json} (100%) rename tests/components/tuya/fixtures/{wsdcg_temperature_humidity.json => wsdcg_g2y6z3p3ja2qhyav.json} (100%) rename tests/components/tuya/fixtures/{wxkg_wireless_switch.json => wxkg_l8yaz4um5b3pwyvf.json} (100%) rename tests/components/tuya/fixtures/{ydkt_dolceclima_unsupported.json => ydkt_jevroj5aguwdbs2e.json} (100%) rename tests/components/tuya/fixtures/{zndb_smart_meter.json => zndb_ze8faryrxr0glqnn.json} (100%) diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index d793b87854a45..040ee1fec2f93 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -14,17 +14,17 @@ from tests.common import MockConfigEntry DEVICE_MOCKS = { - "cl_am43_corded_motor_zigbee_cover": [ + "cl_zah67ekd": [ # https://github.com/home-assistant/core/issues/71242 Platform.COVER, Platform.SELECT, ], - "clkg_curtain_switch": [ + "clkg_nhyj64w2": [ # https://github.com/home-assistant/core/issues/136055 Platform.COVER, Platform.LIGHT, ], - "co2bj_air_detector": [ + "co2bj_yrr3eiyiacm31ski": [ # https://github.com/home-assistant/core/issues/133173 Platform.BINARY_SENSOR, Platform.NUMBER, @@ -32,7 +32,8 @@ Platform.SENSOR, Platform.SIREN, ], - "cs_arete_two_12l_dehumidifier_air_purifier": [ + "cs_ka2wfrdoogpvgzfi": [ + # https://github.com/home-assistant/core/issues/119865 Platform.BINARY_SENSOR, Platform.FAN, Platform.HUMIDIFIER, @@ -40,8 +41,12 @@ Platform.SENSOR, Platform.SWITCH, ], - "cs_emma_dehumidifier": [ + "cs_vmxuxszzjwp5smli": [ # https://github.com/home-assistant/core/issues/119865 + Platform.FAN, + Platform.HUMIDIFIER, + ], + "cs_zibqa9dutqyaxym2": [ Platform.BINARY_SENSOR, Platform.FAN, Platform.HUMIDIFIER, @@ -49,102 +54,97 @@ Platform.SENSOR, Platform.SWITCH, ], - "cs_smart_dry_plus": [ - # https://github.com/home-assistant/core/issues/119865 - Platform.FAN, - Platform.HUMIDIFIER, - ], - "cwjwq_smart_odor_eliminator": [ + "cwjwq_agwu93lr": [ # https://github.com/orgs/home-assistant/discussions/79 Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ], - "cwwsq_cleverio_pf100": [ + "cwwsq_wfkzyy0evslzsmoi": [ # https://github.com/home-assistant/core/issues/144745 Platform.NUMBER, Platform.SENSOR, ], - "cwysj_pixi_smart_drinking_fountain": [ + "cwysj_z3rpyvznfcch99aa": [ # https://github.com/home-assistant/core/pull/146599 Platform.SENSOR, Platform.SWITCH, ], - "cz_dual_channel_metering": [ + "cz_2jxesipczks0kdct": [ # https://github.com/home-assistant/core/issues/147149 Platform.SENSOR, Platform.SWITCH, ], - "dj_smart_light_bulb": [ + "dj_mki13ie507rlry4r": [ # https://github.com/home-assistant/core/pull/126242 Platform.LIGHT ], - "dlq_earu_electric_eawcpt": [ + "dlq_0tnvg2xaisqdadcf": [ # https://github.com/home-assistant/core/issues/102769 Platform.SENSOR, Platform.SWITCH, ], - "dlq_metering_3pn_wifi": [ + "dlq_kxdr6su0c55p7bbo": [ # https://github.com/home-assistant/core/issues/143499 Platform.SENSOR, ], - "gyd_night_light": [ + "gyd_lgekqfxdabipm3tn": [ # https://github.com/home-assistant/core/issues/133173 Platform.LIGHT, ], - "kg_smart_valve": [ + "kg_gbm9ata1zrzaez4a": [ # https://github.com/home-assistant/core/issues/148347 Platform.SWITCH, ], - "kj_bladeless_tower_fan": [ + "kj_yrzylxax1qspdgpp": [ # https://github.com/orgs/home-assistant/discussions/61 Platform.FAN, Platform.SELECT, Platform.SWITCH, ], - "ks_tower_fan": [ + "ks_j9fa8ahzac8uvlfl": [ # https://github.com/orgs/home-assistant/discussions/329 Platform.FAN, Platform.LIGHT, Platform.SWITCH, ], - "kt_serenelife_slpac905wuk_air_conditioner": [ + "kt_5wnlzekkstwcdsvm": [ # https://github.com/home-assistant/core/pull/148646 Platform.CLIMATE, ], - "mal_alarm_host": [ + "mal_gyitctrjj1kefxp2": [ # Alarm Host support Platform.ALARM_CONTROL_PANEL, Platform.NUMBER, Platform.SWITCH, ], - "mcs_door_sensor": [ + "mcs_7jIGJAymiH8OsFFb": [ # https://github.com/home-assistant/core/issues/108301 Platform.BINARY_SENSOR, Platform.SENSOR, ], - "qccdz_ac_charging_control": [ + "qccdz_7bvgooyjhiua1yyq": [ # https://github.com/home-assistant/core/issues/136207 Platform.SWITCH, ], - "qxj_temp_humidity_external_probe": [ - # https://github.com/home-assistant/core/issues/136472 + "qxj_fsea1lat3vuktbt6": [ + # https://github.com/orgs/home-assistant/discussions/318 Platform.SENSOR, ], - "qxj_weather_station": [ - # https://github.com/orgs/home-assistant/discussions/318 + "qxj_is2indt9nlth6esa": [ + # https://github.com/home-assistant/core/issues/136472 Platform.SENSOR, ], - "rqbj_gas_sensor": [ + "rqbj_4iqe2hsfyd86kwwc": [ # https://github.com/orgs/home-assistant/discussions/100 Platform.BINARY_SENSOR, Platform.SENSOR, ], - "sfkzq_valve_controller": [ + "sfkzq_o6dagifntoafakst": [ # https://github.com/home-assistant/core/issues/148116 Platform.SWITCH, ], - "tdq_4_443": [ + "tdq_cq1p0nt0a4rixnex": [ # https://github.com/home-assistant/core/issues/146845 Platform.SELECT, Platform.SWITCH, @@ -155,32 +155,32 @@ Platform.SENSOR, Platform.SWITCH, ], - "wk_air_conditioner": [ + "wk_aqoouq7x": [ # https://github.com/home-assistant/core/issues/146263 Platform.CLIMATE, Platform.SWITCH, ], - "ydkt_dolceclima_unsupported": [ - # https://github.com/orgs/home-assistant/discussions/288 - # unsupported device - no platforms - ], - "wk_wifi_smart_gas_boiler_thermostat": [ + "wk_fi6dne5tu4t1nm6j": [ # https://github.com/orgs/home-assistant/discussions/243 Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ], - "wsdcg_temperature_humidity": [ + "wsdcg_g2y6z3p3ja2qhyav": [ # https://github.com/home-assistant/core/issues/102769 Platform.SENSOR, ], - "wxkg_wireless_switch": [ + "wxkg_l8yaz4um5b3pwyvf": [ # https://github.com/home-assistant/core/issues/93975 Platform.EVENT, Platform.SENSOR, ], - "zndb_smart_meter": [ + "ydkt_jevroj5aguwdbs2e": [ + # https://github.com/orgs/home-assistant/discussions/288 + # unsupported device - no platforms + ], + "zndb_ze8faryrxr0glqnn": [ # https://github.com/home-assistant/core/issues/138372 Platform.SENSOR, ], diff --git a/tests/components/tuya/fixtures/cl_am43_corded_motor_zigbee_cover.json b/tests/components/tuya/fixtures/cl_zah67ekd.json similarity index 100% rename from tests/components/tuya/fixtures/cl_am43_corded_motor_zigbee_cover.json rename to tests/components/tuya/fixtures/cl_zah67ekd.json diff --git a/tests/components/tuya/fixtures/clkg_curtain_switch.json b/tests/components/tuya/fixtures/clkg_nhyj64w2.json similarity index 100% rename from tests/components/tuya/fixtures/clkg_curtain_switch.json rename to tests/components/tuya/fixtures/clkg_nhyj64w2.json diff --git a/tests/components/tuya/fixtures/co2bj_air_detector.json b/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json similarity index 100% rename from tests/components/tuya/fixtures/co2bj_air_detector.json rename to tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json diff --git a/tests/components/tuya/fixtures/cs_emma_dehumidifier.json b/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json similarity index 100% rename from tests/components/tuya/fixtures/cs_emma_dehumidifier.json rename to tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json diff --git a/tests/components/tuya/fixtures/cs_smart_dry_plus.json b/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json similarity index 100% rename from tests/components/tuya/fixtures/cs_smart_dry_plus.json rename to tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json diff --git a/tests/components/tuya/fixtures/cs_arete_two_12l_dehumidifier_air_purifier.json b/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json similarity index 100% rename from tests/components/tuya/fixtures/cs_arete_two_12l_dehumidifier_air_purifier.json rename to tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json diff --git a/tests/components/tuya/fixtures/cwjwq_smart_odor_eliminator.json b/tests/components/tuya/fixtures/cwjwq_agwu93lr.json similarity index 100% rename from tests/components/tuya/fixtures/cwjwq_smart_odor_eliminator.json rename to tests/components/tuya/fixtures/cwjwq_agwu93lr.json diff --git a/tests/components/tuya/fixtures/cwwsq_cleverio_pf100.json b/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json similarity index 100% rename from tests/components/tuya/fixtures/cwwsq_cleverio_pf100.json rename to tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json diff --git a/tests/components/tuya/fixtures/cwysj_pixi_smart_drinking_fountain.json b/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json similarity index 100% rename from tests/components/tuya/fixtures/cwysj_pixi_smart_drinking_fountain.json rename to tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json diff --git a/tests/components/tuya/fixtures/cz_dual_channel_metering.json b/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json similarity index 100% rename from tests/components/tuya/fixtures/cz_dual_channel_metering.json rename to tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json diff --git a/tests/components/tuya/fixtures/dj_smart_light_bulb.json b/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json similarity index 100% rename from tests/components/tuya/fixtures/dj_smart_light_bulb.json rename to tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json diff --git a/tests/components/tuya/fixtures/dlq_earu_electric_eawcpt.json b/tests/components/tuya/fixtures/dlq_0tnvg2xaisqdadcf.json similarity index 100% rename from tests/components/tuya/fixtures/dlq_earu_electric_eawcpt.json rename to tests/components/tuya/fixtures/dlq_0tnvg2xaisqdadcf.json diff --git a/tests/components/tuya/fixtures/dlq_metering_3pn_wifi.json b/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json similarity index 100% rename from tests/components/tuya/fixtures/dlq_metering_3pn_wifi.json rename to tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json diff --git a/tests/components/tuya/fixtures/gyd_night_light.json b/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json similarity index 100% rename from tests/components/tuya/fixtures/gyd_night_light.json rename to tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json diff --git a/tests/components/tuya/fixtures/kg_smart_valve.json b/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json similarity index 100% rename from tests/components/tuya/fixtures/kg_smart_valve.json rename to tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json diff --git a/tests/components/tuya/fixtures/kj_bladeless_tower_fan.json b/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json similarity index 100% rename from tests/components/tuya/fixtures/kj_bladeless_tower_fan.json rename to tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json diff --git a/tests/components/tuya/fixtures/ks_tower_fan.json b/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json similarity index 100% rename from tests/components/tuya/fixtures/ks_tower_fan.json rename to tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json diff --git a/tests/components/tuya/fixtures/kt_serenelife_slpac905wuk_air_conditioner.json b/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json similarity index 100% rename from tests/components/tuya/fixtures/kt_serenelife_slpac905wuk_air_conditioner.json rename to tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json diff --git a/tests/components/tuya/fixtures/mal_alarm_host.json b/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json similarity index 100% rename from tests/components/tuya/fixtures/mal_alarm_host.json rename to tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json diff --git a/tests/components/tuya/fixtures/mcs_door_sensor.json b/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json similarity index 100% rename from tests/components/tuya/fixtures/mcs_door_sensor.json rename to tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json diff --git a/tests/components/tuya/fixtures/qccdz_ac_charging_control.json b/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json similarity index 100% rename from tests/components/tuya/fixtures/qccdz_ac_charging_control.json rename to tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json diff --git a/tests/components/tuya/fixtures/qxj_weather_station.json b/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json similarity index 100% rename from tests/components/tuya/fixtures/qxj_weather_station.json rename to tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json diff --git a/tests/components/tuya/fixtures/qxj_temp_humidity_external_probe.json b/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json similarity index 100% rename from tests/components/tuya/fixtures/qxj_temp_humidity_external_probe.json rename to tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json diff --git a/tests/components/tuya/fixtures/rqbj_gas_sensor.json b/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json similarity index 100% rename from tests/components/tuya/fixtures/rqbj_gas_sensor.json rename to tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json diff --git a/tests/components/tuya/fixtures/sfkzq_valve_controller.json b/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json similarity index 100% rename from tests/components/tuya/fixtures/sfkzq_valve_controller.json rename to tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json diff --git a/tests/components/tuya/fixtures/tdq_4_443.json b/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json similarity index 100% rename from tests/components/tuya/fixtures/tdq_4_443.json rename to tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json diff --git a/tests/components/tuya/fixtures/wk_air_conditioner.json b/tests/components/tuya/fixtures/wk_aqoouq7x.json similarity index 100% rename from tests/components/tuya/fixtures/wk_air_conditioner.json rename to tests/components/tuya/fixtures/wk_aqoouq7x.json diff --git a/tests/components/tuya/fixtures/wk_wifi_smart_gas_boiler_thermostat.json b/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json similarity index 100% rename from tests/components/tuya/fixtures/wk_wifi_smart_gas_boiler_thermostat.json rename to tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json diff --git a/tests/components/tuya/fixtures/wsdcg_temperature_humidity.json b/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json similarity index 100% rename from tests/components/tuya/fixtures/wsdcg_temperature_humidity.json rename to tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json diff --git a/tests/components/tuya/fixtures/wxkg_wireless_switch.json b/tests/components/tuya/fixtures/wxkg_l8yaz4um5b3pwyvf.json similarity index 100% rename from tests/components/tuya/fixtures/wxkg_wireless_switch.json rename to tests/components/tuya/fixtures/wxkg_l8yaz4um5b3pwyvf.json diff --git a/tests/components/tuya/fixtures/ydkt_dolceclima_unsupported.json b/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json similarity index 100% rename from tests/components/tuya/fixtures/ydkt_dolceclima_unsupported.json rename to tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json diff --git a/tests/components/tuya/fixtures/zndb_smart_meter.json b/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json similarity index 100% rename from tests/components/tuya/fixtures/zndb_smart_meter.json rename to tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json diff --git a/tests/components/tuya/snapshots/test_alarm_control_panel.ambr b/tests/components/tuya/snapshots/test_alarm_control_panel.ambr index 97076d5e4679e..73072dcb51668 100644 --- a/tests/components/tuya/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/tuya/snapshots/test_alarm_control_panel.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[mal_alarm_host][alarm_control_panel.multifunction_alarm-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][alarm_control_panel.multifunction_alarm-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][alarm_control_panel.multifunction_alarm-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][alarm_control_panel.multifunction_alarm-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, diff --git a/tests/components/tuya/snapshots/test_binary_sensor.ambr b/tests/components/tuya/snapshots/test_binary_sensor.ambr index 267f61aabd02c..727e59590a501 100644 --- a/tests/components/tuya/snapshots/test_binary_sensor.ambr +++ b/tests/components/tuya/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][binary_sensor.aqi_safety-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][binary_sensor.aqi_safety-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][binary_sensor.aqi_safety-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][binary_sensor.aqi_safety-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'safety', @@ -48,7 +48,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_defrost-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_defrost-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,7 +61,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.dehumidifier_defrost', + 'entity_id': 'binary_sensor.dehumidifer_defrost', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -79,25 +79,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'defrost', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqdefrost', + 'unique_id': 'tuya.mock_device_iddefrost', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_defrost-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_defrost-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Defrost', + 'friendly_name': 'Dehumidifer Defrost', }), 'context': , - 'entity_id': 'binary_sensor.dehumidifier_defrost', + 'entity_id': 'binary_sensor.dehumidifer_defrost', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_tank_full-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_tank_full-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -110,7 +110,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.dehumidifier_tank_full', + 'entity_id': 'binary_sensor.dehumidifer_tank_full', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -128,25 +128,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tankfull', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqtankfull', + 'unique_id': 'tuya.mock_device_idtankfull', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_tank_full-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_tank_full-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Tank full', + 'friendly_name': 'Dehumidifer Tank full', }), 'context': , - 'entity_id': 'binary_sensor.dehumidifier_tank_full', + 'entity_id': 'binary_sensor.dehumidifer_tank_full', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_wet-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_defrost-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -159,7 +159,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.dehumidifier_wet', + 'entity_id': 'binary_sensor.dehumidifier_defrost', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -171,31 +171,31 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wet', + 'original_name': 'Defrost', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'wet', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqwet', + 'translation_key': 'defrost', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqdefrost', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_wet-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_defrost-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Wet', + 'friendly_name': 'Dehumidifier Defrost', }), 'context': , - 'entity_id': 'binary_sensor.dehumidifier_wet', + 'entity_id': 'binary_sensor.dehumidifier_defrost', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_defrost-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_tank_full-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -208,7 +208,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.dehumidifer_defrost', + 'entity_id': 'binary_sensor.dehumidifier_tank_full', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -220,31 +220,31 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Defrost', + 'original_name': 'Tank full', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'defrost', - 'unique_id': 'tuya.mock_device_iddefrost', + 'translation_key': 'tankfull', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqtankfull', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_defrost-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_tank_full-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Dehumidifer Defrost', + 'friendly_name': 'Dehumidifier Tank full', }), 'context': , - 'entity_id': 'binary_sensor.dehumidifer_defrost', + 'entity_id': 'binary_sensor.dehumidifier_tank_full', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_tank_full-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_wet-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -257,7 +257,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.dehumidifer_tank_full', + 'entity_id': 'binary_sensor.dehumidifier_wet', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -269,31 +269,31 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Tank full', + 'original_name': 'Wet', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'tankfull', - 'unique_id': 'tuya.mock_device_idtankfull', + 'translation_key': 'wet', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqwet', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_tank_full-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_wet-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Dehumidifer Tank full', + 'friendly_name': 'Dehumidifier Wet', }), 'context': , - 'entity_id': 'binary_sensor.dehumidifer_tank_full', + 'entity_id': 'binary_sensor.dehumidifier_wet', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][binary_sensor.door_garage_door-entry] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -328,7 +328,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][binary_sensor.door_garage_door-state] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'door', @@ -342,7 +342,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][binary_sensor.gas_sensor_gas-entry] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -377,7 +377,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][binary_sensor.gas_sensor_gas-state] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'gas', diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index 6e93a1b263c16..cb535cc5c0778 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[kt_serenelife_slpac905wuk_air_conditioner][climate.air_conditioner-entry] +# name: test_platform_setup_and_discovery[kt_5wnlzekkstwcdsvm][climate.air_conditioner-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -46,7 +46,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kt_serenelife_slpac905wuk_air_conditioner][climate.air_conditioner-state] +# name: test_platform_setup_and_discovery[kt_5wnlzekkstwcdsvm][climate.air_conditioner-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 22.0, @@ -74,7 +74,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][climate.clima_cucina-entry] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][climate.clima_cucina-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -124,7 +124,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][climate.clima_cucina-state] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][climate.clima_cucina-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 27.0, @@ -155,7 +155,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][climate.wifi_smart_gas_boiler_thermostat-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][climate.wifi_smart_gas_boiler_thermostat-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -198,7 +198,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][climate.wifi_smart_gas_boiler_thermostat-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][climate.wifi_smart_gas_boiler_thermostat-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 24.9, diff --git a/tests/components/tuya/snapshots/test_cover.ambr b/tests/components/tuya/snapshots/test_cover.ambr index 6ae4781c7c123..aa592b25520ea 100644 --- a/tests/components/tuya/snapshots/test_cover.ambr +++ b/tests/components/tuya/snapshots/test_cover.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][cover.kitchen_blinds_curtain-entry] +# name: test_platform_setup_and_discovery[cl_zah67ekd][cover.kitchen_blinds_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][cover.kitchen_blinds_curtain-state] +# name: test_platform_setup_and_discovery[cl_zah67ekd][cover.kitchen_blinds_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 48, @@ -50,7 +50,7 @@ 'state': 'open', }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][cover.tapparelle_studio_curtain-entry] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][cover.tapparelle_studio_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -85,7 +85,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][cover.tapparelle_studio_curtain-state] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][cover.tapparelle_studio_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 0, diff --git a/tests/components/tuya/snapshots/test_diagnostics.ambr b/tests/components/tuya/snapshots/test_diagnostics.ambr index 5fc3796d1096a..93cc0cd0b6d76 100644 --- a/tests/components/tuya/snapshots/test_diagnostics.ambr +++ b/tests/components/tuya/snapshots/test_diagnostics.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_device_diagnostics[rqbj_gas_sensor] +# name: test_device_diagnostics[rqbj_4iqe2hsfyd86kwwc] dict({ 'active_time': '2025-06-24T20:33:10+00:00', 'category': 'rqbj', @@ -88,7 +88,7 @@ 'update_time': '2025-06-24T20:33:10+00:00', }) # --- -# name: test_entry_diagnostics[rqbj_gas_sensor] +# name: test_entry_diagnostics[rqbj_4iqe2hsfyd86kwwc] dict({ 'devices': list([ dict({ diff --git a/tests/components/tuya/snapshots/test_event.ambr b/tests/components/tuya/snapshots/test_event.ambr index 085ebd3ec8ba8..ea19ff486da5e 100644 --- a/tests/components/tuya/snapshots/test_event.ambr +++ b/tests/components/tuya/snapshots/test_event.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_1-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_1-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', @@ -58,7 +58,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_2-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_2-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', diff --git a/tests/components/tuya/snapshots/test_fan.ambr b/tests/components/tuya/snapshots/test_fan.ambr index ff795c150c9c1..69eb1b467e9ae 100644 --- a/tests/components/tuya/snapshots/test_fan.ambr +++ b/tests/components/tuya/snapshots/test_fan.ambr @@ -1,10 +1,12 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][fan.dehumidifier-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][fan.dehumidifer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ + 'preset_modes': list([ + ]), }), 'config_entry_id': , 'config_subentry_id': , @@ -13,7 +15,7 @@ 'disabled_by': None, 'domain': 'fan', 'entity_category': None, - 'entity_id': 'fan.dehumidifier', + 'entity_id': 'fan.dehumidifer', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -29,34 +31,34 @@ 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.bf3fce6af592f12df3gbgq', + 'unique_id': 'tuya.mock_device_id', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][fan.dehumidifier-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][fan.dehumidifer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier', - 'supported_features': , + 'friendly_name': 'Dehumidifer', + 'preset_modes': list([ + ]), + 'supported_features': , }), 'context': , - 'entity_id': 'fan.dehumidifier', + 'entity_id': 'fan.dehumidifer', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][fan.dehumidifer-entry] +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'preset_modes': list([ - ]), }), 'config_entry_id': , 'config_subentry_id': , @@ -65,7 +67,7 @@ 'disabled_by': None, 'domain': 'fan', 'entity_category': None, - 'entity_id': 'fan.dehumidifer', + 'entity_id': 'fan.dehumidifier', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -81,29 +83,27 @@ 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': 0, 'translation_key': None, 'unique_id': 'tuya.mock_device_id', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][fan.dehumidifer-state] +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifer', - 'preset_modes': list([ - ]), - 'supported_features': , + 'friendly_name': 'Dehumidifier ', + 'supported_features': , }), 'context': , - 'entity_id': 'fan.dehumidifer', + 'entity_id': 'fan.dehumidifier', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][fan.dehumidifier-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][fan.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -133,27 +133,27 @@ 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': 0, + 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.mock_device_id', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgq', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][fan.dehumidifier-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][fan.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier ', - 'supported_features': , + 'friendly_name': 'Dehumidifier', + 'supported_features': , }), 'context': , 'entity_id': 'fan.dehumidifier', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][fan.bree-entry] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -192,7 +192,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][fan.bree-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree', @@ -210,7 +210,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][fan.tower_fan_ca_407g_smart-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][fan.tower_fan_ca_407g_smart-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -251,7 +251,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][fan.tower_fan_ca_407g_smart-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][fan.tower_fan_ca_407g_smart-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Tower Fan CA-407G Smart', diff --git a/tests/components/tuya/snapshots/test_humidifier.ambr b/tests/components/tuya/snapshots/test_humidifier.ambr index 3389f927eb491..25bb1799dc853 100644 --- a/tests/components/tuya/snapshots/test_humidifier.ambr +++ b/tests/components/tuya/snapshots/test_humidifier.ambr @@ -1,12 +1,12 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][humidifier.dehumidifier-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][humidifier.dehumidifer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'max_humidity': 70, - 'min_humidity': 35, + 'max_humidity': 80, + 'min_humidity': 25, }), 'config_entry_id': , 'config_subentry_id': , @@ -15,7 +15,7 @@ 'disabled_by': None, 'domain': 'humidifier', 'entity_category': None, - 'entity_id': 'humidifier.dehumidifier', + 'entity_id': 'humidifier.dehumidifer', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -33,37 +33,35 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqswitch', + 'unique_id': 'tuya.mock_device_idswitch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][humidifier.dehumidifier-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][humidifier.dehumidifer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_humidity': 47, 'device_class': 'dehumidifier', - 'friendly_name': 'Dehumidifier', - 'humidity': 50, - 'max_humidity': 70, - 'min_humidity': 35, + 'friendly_name': 'Dehumidifer', + 'max_humidity': 80, + 'min_humidity': 25, 'supported_features': , }), 'context': , - 'entity_id': 'humidifier.dehumidifier', + 'entity_id': 'humidifier.dehumidifer', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][humidifier.dehumidifer-entry] +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'max_humidity': 80, - 'min_humidity': 25, + 'max_humidity': 100, + 'min_humidity': 0, }), 'config_entry_id': , 'config_subentry_id': , @@ -72,7 +70,7 @@ 'disabled_by': None, 'domain': 'humidifier', 'entity_category': None, - 'entity_id': 'humidifier.dehumidifer', + 'entity_id': 'humidifier.dehumidifier', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -94,31 +92,31 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][humidifier.dehumidifer-state] +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'dehumidifier', - 'friendly_name': 'Dehumidifer', - 'max_humidity': 80, - 'min_humidity': 25, + 'friendly_name': 'Dehumidifier ', + 'max_humidity': 100, + 'min_humidity': 0, 'supported_features': , }), 'context': , - 'entity_id': 'humidifier.dehumidifer', + 'entity_id': 'humidifier.dehumidifier', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][humidifier.dehumidifier-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][humidifier.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'max_humidity': 100, - 'min_humidity': 0, + 'max_humidity': 70, + 'min_humidity': 35, }), 'config_entry_id': , 'config_subentry_id': , @@ -145,17 +143,19 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.mock_device_idswitch', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqswitch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][humidifier.dehumidifier-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][humidifier.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'current_humidity': 47, 'device_class': 'dehumidifier', - 'friendly_name': 'Dehumidifier ', - 'max_humidity': 100, - 'min_humidity': 0, + 'friendly_name': 'Dehumidifier', + 'humidity': 50, + 'max_humidity': 70, + 'min_humidity': 35, 'supported_features': , }), 'context': , @@ -163,6 +163,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index fc30460bcc07b..61e77b8e1b46f 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_unsupported_device[ydkt_dolceclima_unsupported] +# name: test_unsupported_device[ydkt_jevroj5aguwdbs2e] list([ DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index ec8e663f62ca6..06ad884cfa3de 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[clkg_curtain_switch][light.tapparelle_studio_backlight-entry] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][light.tapparelle_studio_backlight-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -38,7 +38,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][light.tapparelle_studio_backlight-state] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][light.tapparelle_studio_backlight-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': , @@ -56,7 +56,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dj_smart_light_bulb][light.garage_light-entry] +# name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -96,7 +96,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_smart_light_bulb][light.garage_light-state] +# name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': 138, @@ -119,7 +119,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[gyd_night_light][light.colorful_pir_night_light-entry] +# name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -163,7 +163,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[gyd_night_light][light.colorful_pir_night_light-state] +# name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': None, @@ -192,7 +192,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][light.tower_fan_ca_407g_smart_backlight-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][light.tower_fan_ca_407g_smart_backlight-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -231,7 +231,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][light.tower_fan_ca_407g_smart_backlight-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][light.tower_fan_ca_407g_smart_backlight-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': , diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index 1c8af00baff3e..c6f2bb363b67d 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][number.aqi_alarm_duration-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][number.aqi_alarm_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][number.aqi_alarm_duration-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][number.aqi_alarm_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -58,7 +58,7 @@ 'state': '1.0', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][number.cleverio_pf100_feed-entry] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][number.cleverio_pf100_feed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][number.cleverio_pf100_feed-state] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][number.cleverio_pf100_feed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Cleverio PF100 Feed', @@ -116,7 +116,7 @@ 'state': '1.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_alarm_delay-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -156,7 +156,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_alarm_delay-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -175,7 +175,7 @@ 'state': '20.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_arm_delay-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_arm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -215,7 +215,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_arm_delay-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_arm_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -234,7 +234,7 @@ 'state': '15.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_siren_duration-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_siren_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -274,7 +274,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_siren_duration-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_siren_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -293,7 +293,7 @@ 'state': '3.0', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -333,7 +333,7 @@ 'unit_of_measurement': '℃', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][number.wifi_smart_gas_boiler_thermostat_temperature_correction-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Temperature correction', diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 0f53018412283..4bd058517beae 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][select.kitchen_blinds_motor_mode-entry] +# name: test_platform_setup_and_discovery[cl_zah67ekd][select.kitchen_blinds_motor_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][select.kitchen_blinds_motor_mode-state] +# name: test_platform_setup_and_discovery[cl_zah67ekd][select.kitchen_blinds_motor_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Kitchen Blinds Motor mode', @@ -56,7 +56,7 @@ 'state': 'forward', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][select.aqi_volume-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][select.aqi_volume-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][select.aqi_volume-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][select.aqi_volume-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI Volume', @@ -117,7 +117,7 @@ 'state': 'low', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][select.dehumidifier_countdown-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][select.dehumidifer_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -137,7 +137,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.dehumidifier_countdown', + 'entity_id': 'select.dehumidifer_countdown', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -155,14 +155,14 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'countdown', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqcountdown_set', + 'unique_id': 'tuya.mock_device_idcountdown_set', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][select.dehumidifier_countdown-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][select.dehumidifer_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier Countdown', + 'friendly_name': 'Dehumidifer Countdown', 'options': list([ 'cancel', '1h', @@ -171,14 +171,14 @@ ]), }), 'context': , - 'entity_id': 'select.dehumidifier_countdown', + 'entity_id': 'select.dehumidifer_countdown', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'cancel', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][select.dehumidifer_countdown-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][select.dehumidifier_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -198,7 +198,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.dehumidifer_countdown', + 'entity_id': 'select.dehumidifier_countdown', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -216,14 +216,14 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'countdown', - 'unique_id': 'tuya.mock_device_idcountdown_set', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqcountdown_set', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][select.dehumidifer_countdown-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][select.dehumidifier_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifer Countdown', + 'friendly_name': 'Dehumidifier Countdown', 'options': list([ 'cancel', '1h', @@ -232,14 +232,14 @@ ]), }), 'context': , - 'entity_id': 'select.dehumidifer_countdown', + 'entity_id': 'select.dehumidifier_countdown', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'cancel', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][select.smart_odor_eliminator_pro_odor_elimination_mode-entry] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][select.smart_odor_eliminator_pro_odor_elimination_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -279,7 +279,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][select.smart_odor_eliminator_pro_odor_elimination_mode-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][select.smart_odor_eliminator_pro_odor_elimination_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Odor elimination mode', @@ -296,7 +296,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][select.bree_countdown-entry] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -340,7 +340,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][select.bree_countdown-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree Countdown', @@ -361,7 +361,7 @@ 'state': 'cancel', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][select.4_433_power_on_behavior-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -402,7 +402,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][select.4_433_power_on_behavior-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '4-433 Power on behavior', diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 80051a08396cc..882839a666506 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_battery-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -36,7 +36,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_battery-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -52,7 +52,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_formaldehyde-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_formaldehyde-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -89,7 +89,7 @@ 'unit_of_measurement': 'mg/m3', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_formaldehyde-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_formaldehyde-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI Formaldehyde', @@ -104,7 +104,7 @@ 'state': '0.002', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_humidity-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -141,7 +141,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_humidity-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -157,7 +157,7 @@ 'state': '53.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_temperature-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -197,7 +197,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_temperature-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -213,7 +213,7 @@ 'state': '26.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_volatile_organic_compounds-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_volatile_organic_compounds-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -250,7 +250,7 @@ 'unit_of_measurement': 'mg/m³', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_volatile_organic_compounds-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_volatile_organic_compounds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'volatile_organic_compounds', @@ -266,7 +266,7 @@ 'state': '0.018', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][sensor.dehumidifier_humidity-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][sensor.dehumidifer_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -281,7 +281,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.dehumidifier_humidity', + 'entity_id': 'sensor.dehumidifer_humidity', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -299,27 +299,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqhumidity_indoor', + 'unique_id': 'tuya.mock_device_idhumidity_indoor', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][sensor.dehumidifier_humidity-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][sensor.dehumidifer_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', - 'friendly_name': 'Dehumidifier Humidity', + 'friendly_name': 'Dehumidifer Humidity', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.dehumidifier_humidity', + 'entity_id': 'sensor.dehumidifer_humidity', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '47.0', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][sensor.dehumidifer_humidity-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][sensor.dehumidifier_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -334,7 +334,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.dehumidifer_humidity', + 'entity_id': 'sensor.dehumidifier_humidity', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -352,27 +352,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', - 'unique_id': 'tuya.mock_device_idhumidity_indoor', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqhumidity_indoor', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][sensor.dehumidifer_humidity-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][sensor.dehumidifier_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', - 'friendly_name': 'Dehumidifer Humidity', + 'friendly_name': 'Dehumidifier Humidity', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.dehumidifer_humidity', + 'entity_id': 'sensor.dehumidifier_humidity', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '47.0', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_battery-entry] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -409,7 +409,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_battery-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -425,7 +425,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_status-entry] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -460,7 +460,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_status-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_status-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Status', @@ -473,7 +473,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][sensor.cleverio_pf100_last_amount-entry] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][sensor.cleverio_pf100_last_amount-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -510,7 +510,7 @@ 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][sensor.cleverio_pf100_last_amount-state] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][sensor.cleverio_pf100_last_amount-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Cleverio PF100 Last amount', @@ -525,7 +525,7 @@ 'state': '2.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_filter_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_filter_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -565,7 +565,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_filter_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_filter_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -581,7 +581,7 @@ 'state': '18965.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_uv_runtime-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_uv_runtime-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -621,7 +621,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_uv_runtime-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_uv_runtime-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -637,7 +637,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_level-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -672,7 +672,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_level-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Water level', @@ -685,7 +685,7 @@ 'state': 'level_3', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_pump_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_pump_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -725,7 +725,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_pump_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_pump_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -741,7 +741,7 @@ 'state': '18965.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_usage_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_usage_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -781,7 +781,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_usage_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_usage_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -797,7 +797,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_current-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -840,7 +840,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_current-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -856,7 +856,7 @@ 'state': '0.083', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_power-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -896,7 +896,7 @@ 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_power-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -912,7 +912,7 @@ 'state': '6.4', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_voltage-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -955,7 +955,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_voltage-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -971,7 +971,7 @@ 'state': '121.7', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1014,7 +1014,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1030,7 +1030,7 @@ 'state': '2.198', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1070,7 +1070,7 @@ 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1086,7 +1086,7 @@ 'state': '495.3', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1129,7 +1129,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1145,7 +1145,7 @@ 'state': '231.4', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1185,7 +1185,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1201,7 +1201,7 @@ 'state': '0.637', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1241,7 +1241,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1257,7 +1257,7 @@ 'state': '0.108', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1297,7 +1297,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1313,7 +1313,7 @@ 'state': '221.1', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1353,7 +1353,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1369,7 +1369,7 @@ 'state': '11.203', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1409,7 +1409,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1425,7 +1425,7 @@ 'state': '2.41', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1465,7 +1465,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1481,7 +1481,7 @@ 'state': '218.7', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1521,7 +1521,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1537,7 +1537,7 @@ 'state': '0.913', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1577,7 +1577,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1593,7 +1593,7 @@ 'state': '0.092', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1633,7 +1633,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1649,7 +1649,7 @@ 'state': '220.4', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][sensor.door_garage_battery-entry] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1686,7 +1686,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][sensor.door_garage_battery-state] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -1702,7 +1702,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_battery_state-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1715,7 +1715,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.frysen_battery_state', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1733,24 +1733,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_state', - 'unique_id': 'tuya.bff00f6abe0563b284t77pbattery_state', + 'unique_id': 'tuya.bf84c743a84eb2c8abeurzbattery_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_battery_state-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Frysen Battery state', + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Battery state', }), 'context': , - 'entity_id': 'sensor.frysen_battery_state', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'high', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_humidity-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1765,7 +1765,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.frysen_humidity', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1783,27 +1783,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', - 'unique_id': 'tuya.bff00f6abe0563b284t77phumidity_value', + 'unique_id': 'tuya.bf84c743a84eb2c8abeurzhumidity_value', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_humidity-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', - 'friendly_name': 'Frysen Humidity', + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Humidity', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.frysen_humidity', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '38.0', + 'state': '52.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_probe_temperature-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1818,7 +1818,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.frysen_probe_temperature', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1827,39 +1827,36 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Probe temperature', + 'original_name': 'Illuminance', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'temperature_external', - 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current_external', - 'unit_of_measurement': , + 'translation_key': 'illuminance', + 'unique_id': 'tuya.bf84c743a84eb2c8abeurzbright_value', + 'unit_of_measurement': 'lx', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_probe_temperature-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Frysen Probe temperature', + 'device_class': 'illuminance', + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Illuminance', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': 'lx', }), 'context': , - 'entity_id': 'sensor.frysen_probe_temperature', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-13.0', + 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_temperature-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1874,7 +1871,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.frysen_temperature', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1889,46 +1886,48 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Temperature', + 'original_name': 'Probe temperature', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'temperature', - 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current', + 'translation_key': 'temperature_external', + 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current_external', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_temperature-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Frysen Temperature', + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Probe temperature', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.frysen_temperature', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '22.2', + 'state': '-40.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state', + 'entity_category': None, + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1937,48 +1936,52 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery state', + 'original_name': 'Temperature', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'battery_state', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzbattery_state', - 'unit_of_measurement': None, + 'translation_key': 'temperature', + 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current', + 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Battery state', + 'device_class': 'temperature', + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state', + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'high', + 'state': '24.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), + 'capabilities': None, 'config_entry_id': , 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity', + 'entity_category': , + 'entity_id': 'sensor.frysen_battery_state', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1988,35 +1991,32 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Humidity', + 'original_name': 'Battery state', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzhumidity_value', - 'unit_of_measurement': '%', + 'translation_key': 'battery_state', + 'unique_id': 'tuya.bff00f6abe0563b284t77pbattery_state', + 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Humidity', - 'state_class': , - 'unit_of_measurement': '%', + 'friendly_name': 'Frysen Battery state', }), 'context': , - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity', + 'entity_id': 'sensor.frysen_battery_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '52.0', + 'state': 'high', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2031,7 +2031,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance', + 'entity_id': 'sensor.frysen_humidity', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2041,35 +2041,35 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Illuminance', + 'original_name': 'Humidity', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'illuminance', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzbright_value', - 'unit_of_measurement': 'lx', + 'translation_key': 'humidity', + 'unique_id': 'tuya.bff00f6abe0563b284t77phumidity_value', + 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'illuminance', - 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Illuminance', + 'device_class': 'humidity', + 'friendly_name': 'Frysen Humidity', 'state_class': , - 'unit_of_measurement': 'lx', + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance', + 'entity_id': 'sensor.frysen_humidity', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': '38.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_probe_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2084,7 +2084,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature', + 'entity_id': 'sensor.frysen_probe_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2105,27 +2105,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_external', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current_external', + 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current_external', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_probe_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Probe temperature', + 'friendly_name': 'Frysen Probe temperature', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature', + 'entity_id': 'sensor.frysen_probe_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-40.0', + 'state': '-13.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2140,7 +2140,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature', + 'entity_id': 'sensor.frysen_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2161,27 +2161,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current', + 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Temperature', + 'friendly_name': 'Frysen Temperature', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature', + 'entity_id': 'sensor.frysen_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '24.0', + 'state': '22.2', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][sensor.gas_sensor_gas-entry] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2218,7 +2218,7 @@ 'unit_of_measurement': 'ppm', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][sensor.gas_sensor_gas-state] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][sensor.gas_sensor_gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Gas sensor Gas', @@ -2334,7 +2334,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2371,7 +2371,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][sensor.wifi_smart_gas_boiler_thermostat_battery-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][sensor.wifi_smart_gas_boiler_thermostat_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2387,7 +2387,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_battery-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2424,7 +2424,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_battery-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2440,7 +2440,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_humidity-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2477,7 +2477,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_humidity-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2493,7 +2493,7 @@ 'state': '47.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_temperature-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2533,7 +2533,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_temperature-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2549,7 +2549,7 @@ 'state': '18.5', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][sensor.bathroom_smart_switch_battery-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][sensor.bathroom_smart_switch_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2586,7 +2586,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][sensor.bathroom_smart_switch_battery-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][sensor.bathroom_smart_switch_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2602,7 +2602,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_current-entry] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2642,7 +2642,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_current-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -2658,7 +2658,7 @@ 'state': '5.62', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_power-entry] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2698,7 +2698,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_power-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -2714,7 +2714,7 @@ 'state': '1.185', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_voltage-entry] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2754,7 +2754,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_voltage-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', diff --git a/tests/components/tuya/snapshots/test_siren.ambr b/tests/components/tuya/snapshots/test_siren.ambr index 8a6faa31c4374..7b6afe9dc6057 100644 --- a/tests/components/tuya/snapshots/test_siren.ambr +++ b/tests/components/tuya/snapshots/test_siren.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][siren.aqi-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][siren.aqi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][siren.aqi-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][siren.aqi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI', diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index e21fe9c91bd0c..2c2325e9ed822 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][switch.dehumidifier_child_lock-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,7 +12,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.dehumidifier_child_lock', + 'entity_id': 'switch.dehumidifer_child_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -30,25 +30,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqchild_lock', + 'unique_id': 'tuya.mock_device_idchild_lock', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][switch.dehumidifier_child_lock-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier Child lock', + 'friendly_name': 'Dehumidifer Child lock', 'icon': 'mdi:account-lock', }), 'context': , - 'entity_id': 'switch.dehumidifier_child_lock', + 'entity_id': 'switch.dehumidifer_child_lock', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_child_lock-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_ionizer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,7 +61,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.dehumidifer_child_lock', + 'entity_id': 'switch.dehumidifer_ionizer', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -72,32 +72,32 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:account-lock', - 'original_name': 'Child lock', + 'original_icon': 'mdi:atom', + 'original_name': 'Ionizer', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'child_lock', - 'unique_id': 'tuya.mock_device_idchild_lock', + 'translation_key': 'ionizer', + 'unique_id': 'tuya.mock_device_idanion', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_child_lock-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_ionizer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifer Child lock', - 'icon': 'mdi:account-lock', + 'friendly_name': 'Dehumidifer Ionizer', + 'icon': 'mdi:atom', }), 'context': , - 'entity_id': 'switch.dehumidifer_child_lock', + 'entity_id': 'switch.dehumidifer_ionizer', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_ionizer-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][switch.dehumidifier_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -110,7 +110,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.dehumidifer_ionizer', + 'entity_id': 'switch.dehumidifier_child_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -121,32 +121,32 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:atom', - 'original_name': 'Ionizer', + 'original_icon': 'mdi:account-lock', + 'original_name': 'Child lock', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'ionizer', - 'unique_id': 'tuya.mock_device_idanion', + 'translation_key': 'child_lock', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqchild_lock', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_ionizer-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][switch.dehumidifier_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifer Ionizer', - 'icon': 'mdi:atom', + 'friendly_name': 'Dehumidifier Child lock', + 'icon': 'mdi:account-lock', }), 'context': , - 'entity_id': 'switch.dehumidifer_ionizer', + 'entity_id': 'switch.dehumidifier_child_lock', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][switch.smart_odor_eliminator_pro_switch-entry] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][switch.smart_odor_eliminator_pro_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -181,7 +181,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][switch.smart_odor_eliminator_pro_switch-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][switch.smart_odor_eliminator_pro_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Switch', @@ -194,7 +194,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_filter_reset-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_filter_reset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -229,7 +229,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_filter_reset-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_filter_reset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Filter reset', @@ -242,7 +242,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_power-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -277,7 +277,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_power-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Power', @@ -290,7 +290,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -325,7 +325,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Reset of water usage days', @@ -338,7 +338,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_uv_sterilization-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_uv_sterilization-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -373,7 +373,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_uv_sterilization-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_uv_sterilization-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain UV sterilization', @@ -386,7 +386,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_water_pump_reset-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_water_pump_reset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -421,7 +421,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_water_pump_reset-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_water_pump_reset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Water pump reset', @@ -434,7 +434,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_1-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -469,7 +469,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_1-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -483,7 +483,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_2-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -518,7 +518,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_2-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -532,7 +532,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -567,7 +567,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '一路带计量磁保持通断器 Child lock', @@ -580,7 +580,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -615,7 +615,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '一路带计量磁保持通断器 Switch', @@ -628,7 +628,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[kg_smart_valve][switch.qt_switch_switch_1-entry] +# name: test_platform_setup_and_discovery[kg_gbm9ata1zrzaez4a][switch.qt_switch_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -663,7 +663,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kg_smart_valve][switch.qt_switch_switch_1-state] +# name: test_platform_setup_and_discovery[kg_gbm9ata1zrzaez4a][switch.qt_switch_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -677,7 +677,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][switch.bree_power-entry] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -712,7 +712,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][switch.bree_power-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree Power', @@ -725,7 +725,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][switch.tower_fan_ca_407g_smart_ionizer-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][switch.tower_fan_ca_407g_smart_ionizer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -760,7 +760,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][switch.tower_fan_ca_407g_smart_ionizer-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][switch.tower_fan_ca_407g_smart_ionizer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Tower Fan CA-407G Smart Ionizer', @@ -773,7 +773,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_arm_beep-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_arm_beep-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -808,7 +808,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_arm_beep-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_arm_beep-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Multifunction alarm Arm beep', @@ -821,7 +821,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_siren-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_siren-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -856,7 +856,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_siren-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_siren-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Multifunction alarm Siren', @@ -869,7 +869,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[qccdz_ac_charging_control][switch.ac_charging_control_box_switch-entry] +# name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -904,7 +904,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qccdz_ac_charging_control][switch.ac_charging_control_box_switch-state] +# name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AC charging control box Switch', @@ -917,7 +917,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sfkzq_valve_controller][switch.sprinkler_cesare_switch-entry] +# name: test_platform_setup_and_discovery[sfkzq_o6dagifntoafakst][switch.sprinkler_cesare_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -952,7 +952,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sfkzq_valve_controller][switch.sprinkler_cesare_switch-state] +# name: test_platform_setup_and_discovery[sfkzq_o6dagifntoafakst][switch.sprinkler_cesare_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Sprinkler Cesare Switch', @@ -965,7 +965,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_1-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1000,7 +1000,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_1-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1014,7 +1014,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_2-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1049,7 +1049,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_2-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1063,7 +1063,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_3-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1098,7 +1098,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_3-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1112,7 +1112,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_4-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_4-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1147,7 +1147,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_4-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1209,7 +1209,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][switch.clima_cucina_child_lock-entry] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][switch.clima_cucina_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1244,7 +1244,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][switch.clima_cucina_child_lock-state] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][switch.clima_cucina_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Clima cucina Child lock', @@ -1257,7 +1257,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][switch.wifi_smart_gas_boiler_thermostat_child_lock-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][switch.wifi_smart_gas_boiler_thermostat_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1292,7 +1292,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][switch.wifi_smart_gas_boiler_thermostat_child_lock-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][switch.wifi_smart_gas_boiler_thermostat_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Child lock', diff --git a/tests/components/tuya/test_binary_sensor.py b/tests/components/tuya/test_binary_sensor.py index 9045b28bfa9ef..85dd644b79c7b 100644 --- a/tests/components/tuya/test_binary_sensor.py +++ b/tests/components/tuya/test_binary_sensor.py @@ -60,7 +60,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) @pytest.mark.parametrize( ("fault_value", "tankfull", "defrost", "wet"), @@ -84,7 +84,7 @@ async def test_bitmap( defrost: str, wet: str, ) -> None: - """Test BITMAP fault sensor on cs_arete_two_12l_dehumidifier_air_purifier.""" + """Test BITMAP fault sensor on cs_zibqa9dutqyaxym2.""" await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) assert hass.states.get("binary_sensor.dehumidifier_tank_full").state == "off" diff --git a/tests/components/tuya/test_climate.py b/tests/components/tuya/test_climate.py index e8aee3f4f963c..01fdf469e2779 100644 --- a/tests/components/tuya/test_climate.py +++ b/tests/components/tuya/test_climate.py @@ -66,7 +66,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_set_temperature( hass: HomeAssistant, @@ -96,7 +96,7 @@ async def test_set_temperature( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_fan_mode_windspeed( hass: HomeAssistant, @@ -127,7 +127,7 @@ async def test_fan_mode_windspeed( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_fan_mode_no_valid_code( hass: HomeAssistant, @@ -161,7 +161,7 @@ async def test_fan_mode_no_valid_code( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_set_humidity_not_supported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_cover.py b/tests/components/tuya/test_cover.py index 24e43dcccece9..20d84878a588d 100644 --- a/tests/components/tuya/test_cover.py +++ b/tests/components/tuya/test_cover.py @@ -67,7 +67,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER]) async def test_open_service( @@ -101,7 +101,7 @@ async def test_open_service( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER]) async def test_close_service( @@ -135,7 +135,7 @@ async def test_close_service( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_set_position( hass: HomeAssistant, @@ -168,7 +168,7 @@ async def test_set_position( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @pytest.mark.parametrize( ("percent_control", "percent_state"), @@ -202,7 +202,7 @@ async def test_percent_state_on_cover( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_set_tilt_position_not_supported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_diagnostics.py b/tests/components/tuya/test_diagnostics.py index 2009f117efbb9..f07c2faa229e2 100644 --- a/tests/components/tuya/test_diagnostics.py +++ b/tests/components/tuya/test_diagnostics.py @@ -22,7 +22,7 @@ from tests.typing import ClientSessionGenerator -@pytest.mark.parametrize("mock_device_code", ["rqbj_gas_sensor"]) +@pytest.mark.parametrize("mock_device_code", ["rqbj_4iqe2hsfyd86kwwc"]) async def test_entry_diagnostics( hass: HomeAssistant, mock_manager: ManagerCompat, @@ -43,7 +43,7 @@ async def test_entry_diagnostics( ) -@pytest.mark.parametrize("mock_device_code", ["rqbj_gas_sensor"]) +@pytest.mark.parametrize("mock_device_code", ["rqbj_4iqe2hsfyd86kwwc"]) async def test_device_diagnostics( hass: HomeAssistant, mock_manager: ManagerCompat, diff --git a/tests/components/tuya/test_humidifier.py b/tests/components/tuya/test_humidifier.py index d4996bcd32a9b..bd3604b25ddb8 100644 --- a/tests/components/tuya/test_humidifier.py +++ b/tests/components/tuya/test_humidifier.py @@ -65,7 +65,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_turn_on( hass: HomeAssistant, @@ -92,7 +92,7 @@ async def test_turn_on( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_turn_off( hass: HomeAssistant, @@ -119,7 +119,7 @@ async def test_turn_off( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_set_humidity( hass: HomeAssistant, @@ -149,7 +149,7 @@ async def test_set_humidity( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_turn_on_unsupported( hass: HomeAssistant, @@ -179,7 +179,7 @@ async def test_turn_on_unsupported( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_turn_off_unsupported( hass: HomeAssistant, @@ -209,7 +209,7 @@ async def test_turn_off_unsupported( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_set_humidity_unsupported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_init.py b/tests/components/tuya/test_init.py index 9e9855f9faca2..ab96f58ecd014 100644 --- a/tests/components/tuya/test_init.py +++ b/tests/components/tuya/test_init.py @@ -15,7 +15,7 @@ from tests.common import MockConfigEntry -@pytest.mark.parametrize("mock_device_code", ["ydkt_dolceclima_unsupported"]) +@pytest.mark.parametrize("mock_device_code", ["ydkt_jevroj5aguwdbs2e"]) async def test_unsupported_device( hass: HomeAssistant, mock_manager: ManagerCompat, diff --git a/tests/components/tuya/test_light.py b/tests/components/tuya/test_light.py index 0d4706a556391..e35866138763e 100644 --- a/tests/components/tuya/test_light.py +++ b/tests/components/tuya/test_light.py @@ -64,7 +64,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["dj_smart_light_bulb"], + ["dj_mki13ie507rlry4r"], ) async def test_turn_on_white( hass: HomeAssistant, @@ -98,7 +98,7 @@ async def test_turn_on_white( @pytest.mark.parametrize( "mock_device_code", - ["dj_smart_light_bulb"], + ["dj_mki13ie507rlry4r"], ) async def test_turn_off( hass: HomeAssistant, diff --git a/tests/components/tuya/test_number.py b/tests/components/tuya/test_number.py index b6c7b1f6de51b..f28d641417020 100644 --- a/tests/components/tuya/test_number.py +++ b/tests/components/tuya/test_number.py @@ -59,7 +59,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["mal_alarm_host"], + ["mal_gyitctrjj1kefxp2"], ) async def test_set_value( hass: HomeAssistant, @@ -89,7 +89,7 @@ async def test_set_value( @pytest.mark.parametrize( "mock_device_code", - ["mal_alarm_host"], + ["mal_gyitctrjj1kefxp2"], ) async def test_set_value_no_function( hass: HomeAssistant, diff --git a/tests/components/tuya/test_select.py b/tests/components/tuya/test_select.py index cd1d926ff76cf..475fab30b90cc 100644 --- a/tests/components/tuya/test_select.py +++ b/tests/components/tuya/test_select.py @@ -62,7 +62,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_select_option( hass: HomeAssistant, @@ -92,7 +92,7 @@ async def test_select_option( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_select_invalid_option( hass: HomeAssistant, From 896062d66981c868c95173fa7d4e4fdc8050e009 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:22:45 +0200 Subject: [PATCH 082/247] Fix Tuya fan speeds with numeric values (#149971) --- homeassistant/components/tuya/fan.py | 4 +- tests/components/tuya/__init__.py | 20 ++ .../tuya/fixtures/cs_qhxmvae667uap4zh.json | 32 +++ .../tuya/fixtures/fs_g0ewlb1vmwqljzji.json | 134 +++++++++++ .../tuya/fixtures/fs_ibytpo6fpnugft1c.json | 23 ++ .../tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json | 86 +++++++ tests/components/tuya/snapshots/test_fan.ambr | 221 ++++++++++++++++++ .../tuya/snapshots/test_humidifier.ambr | 55 +++++ .../components/tuya/snapshots/test_light.ambr | 81 +++++++ .../tuya/snapshots/test_select.ambr | 63 +++++ .../tuya/snapshots/test_switch.ambr | 192 +++++++++++++++ 11 files changed, 910 insertions(+), 1 deletion(-) create mode 100644 tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json create mode 100644 tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json create mode 100644 tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json create mode 100644 tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 90f4132cef075..4c97b857fb726 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -267,7 +267,9 @@ def percentage(self) -> int | None: return int(self._speed.remap_value_to(value, 1, 100)) if self._speeds is not None: - if (value := self.device.status.get(self._speeds.dpcode)) is None: + if ( + value := self.device.status.get(self._speeds.dpcode) + ) is None or value not in self._speeds.range: return None return ordered_list_item_to_percentage(self._speeds.range, value) diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 040ee1fec2f93..a8182adb90c89 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -41,6 +41,11 @@ Platform.SENSOR, Platform.SWITCH, ], + "cs_qhxmvae667uap4zh": [ + # https://github.com/home-assistant/core/issues/141278 + Platform.FAN, + Platform.HUMIDIFIER, + ], "cs_vmxuxszzjwp5smli": [ # https://github.com/home-assistant/core/issues/119865 Platform.FAN, @@ -88,6 +93,16 @@ # https://github.com/home-assistant/core/issues/143499 Platform.SENSOR, ], + "fs_g0ewlb1vmwqljzji": [ + # https://github.com/home-assistant/core/issues/141231 + Platform.FAN, + Platform.LIGHT, + Platform.SELECT, + ], + "fs_ibytpo6fpnugft1c": [ + # https://github.com/home-assistant/core/issues/135541 + Platform.FAN, + ], "gyd_lgekqfxdabipm3tn": [ # https://github.com/home-assistant/core/issues/133173 Platform.LIGHT, @@ -96,6 +111,11 @@ # https://github.com/home-assistant/core/issues/148347 Platform.SWITCH, ], + "kj_CAjWAxBUZt7QZHfz": [ + # https://github.com/home-assistant/core/issues/146023 + Platform.FAN, + Platform.SWITCH, + ], "kj_yrzylxax1qspdgpp": [ # https://github.com/orgs/home-assistant/discussions/61 Platform.FAN, diff --git a/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json b/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json new file mode 100644 index 0000000000000..9b0b704e3dec3 --- /dev/null +++ b/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json @@ -0,0 +1,32 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "28403630e8db84b7a963", + "name": "DryFix", + "category": "cs", + "product_id": "qhxmvae667uap4zh", + "product_name": "", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-04-03T13:10:02+00:00", + "create_time": "2024-04-03T13:10:02+00:00", + "update_time": "2024-04-03T13:10:02+00:00", + "function": {}, + "status_range": { + "fault": { + "type": "Bitmap", + "value": { + "label": ["E1", "E2"] + } + } + }, + "status": { + "fault": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json b/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json new file mode 100644 index 0000000000000..3aae03c904a26 --- /dev/null +++ b/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json @@ -0,0 +1,134 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "XXX", + "name": "Ceiling Fan With Light", + "category": "fs", + "product_id": "g0ewlb1vmwqljzji", + "product_name": "Ceiling Fan With Light", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-22T22:57:04+00:00", + "create_time": "2025-03-22T22:57:04+00:00", + "update_time": "2025-03-22T22:57:04+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["normal", "sleep", "nature"] + } + }, + "fan_speed": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6"] + } + }, + "fan_direction": { + "type": "Enum", + "value": { + "range": ["forward", "reverse"] + } + }, + "light": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "countdown_set": { + "type": "Enum", + "value": { + "range": ["cancel", "1h", "2h", "4h", "8h"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["normal", "sleep", "nature"] + } + }, + "fan_speed": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6"] + } + }, + "fan_direction": { + "type": "Enum", + "value": { + "range": ["forward", "reverse"] + } + }, + "light": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "countdown_set": { + "type": "Enum", + "value": { + "range": ["cancel", "1h", "2h", "4h", "8h"] + } + } + }, + "status": { + "switch": true, + "mode": "normal", + "fan_speed": 1, + "fan_direction": "reverse", + "light": true, + "bright_value": 100, + "temp_value": 0, + "countdown_set": "off" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json b/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json new file mode 100644 index 0000000000000..02b3808f84d15 --- /dev/null +++ b/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json @@ -0,0 +1,23 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "10706550a4e57c88b93a", + "name": "Ventilador Cama", + "category": "fs", + "product_id": "ibytpo6fpnugft1c", + "product_name": "Tower bladeless fan ", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-01-10T18:47:46+00:00", + "create_time": "2025-01-10T18:47:46+00:00", + "update_time": "2025-01-10T18:47:46+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json b/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json new file mode 100644 index 0000000000000..5758fce2152fb --- /dev/null +++ b/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json @@ -0,0 +1,86 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "152027113c6105cce49c", + "name": "HL400", + "category": "kj", + "product_id": "CAjWAxBUZt7QZHfz", + "product_name": "air purifier", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-05-13T11:02:55+00:00", + "create_time": "2025-05-13T11:02:55+00:00", + "update_time": "2025-05-13T11:02:55+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "speed": { + "type": "Enum", + "value": { + "range": ["1", "2", "3"] + } + }, + "anion": { + "type": "Boolean", + "value": {} + }, + "lock": { + "type": "Boolean", + "value": {} + }, + "uv": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "uv": { + "type": "Boolean", + "value": {} + }, + "lock": { + "type": "Boolean", + "value": {} + }, + "anion": { + "type": "Boolean", + "value": {} + }, + "speed": { + "type": "Enum", + "value": { + "range": ["1", "2", "3"] + } + }, + "switch": { + "type": "Boolean", + "value": {} + }, + "pm25": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 500, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": true, + "lock": false, + "anion": true, + "speed": 3, + "uv": true, + "pm25": 45 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_fan.ambr b/tests/components/tuya/snapshots/test_fan.ambr index 69eb1b467e9ae..7532023860b6a 100644 --- a/tests/components/tuya/snapshots/test_fan.ambr +++ b/tests/components/tuya/snapshots/test_fan.ambr @@ -53,6 +53,56 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[cs_qhxmvae667uap4zh][fan.dryfix-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.dryfix', + 'has_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': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.28403630e8db84b7a963', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_qhxmvae667uap4zh][fan.dryfix-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'DryFix', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.dryfix', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -153,6 +203,177 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][fan.ceiling_fan_with_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + 'normal', + 'sleep', + 'nature', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.ceiling_fan_with_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.XXX', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][fan.ceiling_fan_with_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'direction': 'reverse', + 'friendly_name': 'Ceiling Fan With Light', + 'percentage': None, + 'percentage_step': 16.666666666666668, + 'preset_mode': 'normal', + 'preset_modes': list([ + 'normal', + 'sleep', + 'nature', + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.ceiling_fan_with_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[fs_ibytpo6fpnugft1c][fan.ventilador_cama-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.ventilador_cama', + 'has_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': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.10706550a4e57c88b93a', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[fs_ibytpo6fpnugft1c][fan.ventilador_cama-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ventilador Cama', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.ventilador_cama', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][fan.hl400-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.hl400', + 'has_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': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.152027113c6105cce49c', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][fan.hl400-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400', + 'percentage': None, + 'percentage_step': 33.333333333333336, + 'preset_mode': None, + 'preset_modes': list([ + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.hl400', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_humidifier.ambr b/tests/components/tuya/snapshots/test_humidifier.ambr index 25bb1799dc853..33034e3f6e75a 100644 --- a/tests/components/tuya/snapshots/test_humidifier.ambr +++ b/tests/components/tuya/snapshots/test_humidifier.ambr @@ -54,6 +54,61 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[cs_qhxmvae667uap4zh][humidifier.dryfix-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_humidity': 100, + 'min_humidity': 0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'humidifier', + 'entity_category': None, + 'entity_id': 'humidifier.dryfix', + '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': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.28403630e8db84b7a963switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_qhxmvae667uap4zh][humidifier.dryfix-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'dehumidifier', + 'friendly_name': 'DryFix', + 'max_humidity': 100, + 'min_humidity': 0, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.dryfix', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 06ad884cfa3de..37c2a0f81d926 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -119,6 +119,87 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][light.ceiling_fan_with_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ceiling_fan_with_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.XXXlight', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][light.ceiling_fan_with_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'Ceiling Fan With Light', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.ceiling_fan_with_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 4bd058517beae..d3348a5899ec9 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -296,6 +296,69 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][select.ceiling_fan_with_light_countdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '1h', + '2h', + '4h', + '8h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.ceiling_fan_with_light_countdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Countdown', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'countdown', + 'unique_id': 'tuya.XXXcountdown_set', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][select.ceiling_fan_with_light_countdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ceiling Fan With Light Countdown', + 'options': list([ + 'cancel', + '1h', + '2h', + '4h', + '8h', + ]), + }), + 'context': , + 'entity_id': 'select.ceiling_fan_with_light_countdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 2c2325e9ed822..9edc3e6b28508 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -677,6 +677,198 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.hl400_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.152027113c6105cce49clock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 Child lock', + }), + 'context': , + 'entity_id': 'switch.hl400_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_ionizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.hl400_ionizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ionizer', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ionizer', + 'unique_id': 'tuya.152027113c6105cce49canion', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_ionizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 Ionizer', + }), + 'context': , + 'entity_id': 'switch.hl400_ionizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.hl400_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.152027113c6105cce49cswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 Power', + }), + 'context': , + 'entity_id': 'switch.hl400_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_uv_sterilization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.hl400_uv_sterilization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV sterilization', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'uv_sterilization', + 'unique_id': 'tuya.152027113c6105cce49cuv', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_uv_sterilization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 UV sterilization', + }), + 'context': , + 'entity_id': 'switch.hl400_uv_sterilization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From d810b4ca38a816199383f715bd869c62d4bd2762 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 4 Aug 2025 15:58:57 +0200 Subject: [PATCH 083/247] Bump zwave-js-server-python to 0.67.1 (#149972) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 2cad8df3805ae..153e8e6a7fe66 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["zwave_js_server"], - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.67.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.67.1"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index d4fd8f0a0c80e..8df449a5f7bc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3215,7 +3215,7 @@ ziggo-mediabox-xl==1.1.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.67.0 +zwave-js-server-python==0.67.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94de3c485a6a5..f255774e2cc64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2650,7 +2650,7 @@ zeversolar==0.3.2 zha==0.0.65 # homeassistant.components.zwave_js -zwave-js-server-python==0.67.0 +zwave-js-server-python==0.67.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From 7a9966120ef2bd0f5dcc63d16354dc3449fc1fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 4 Aug 2025 15:59:07 +0100 Subject: [PATCH 084/247] Bump hass-nabucasa from 0.110.1 to 0.111.0 (#149977) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 63eae6261d458..0ef407b3628a0 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.110.1"], + "requirements": ["hass-nabucasa==0.111.0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6ebd9a8efb795..b33314c0a4e2d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==4.0.1 -hass-nabucasa==0.110.1 +hass-nabucasa==0.111.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250731.0 diff --git a/pyproject.toml b/pyproject.toml index 5f99bd491d841..457caf054dee4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.110.1", + "hass-nabucasa==0.111.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", diff --git a/requirements.txt b/requirements.txt index ba08a72e324ce..90953842e20ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.110.1 +hass-nabucasa==0.111.0 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8df449a5f7bc4..12dfc25e16174 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ habiticalib==0.4.1 habluetooth==4.0.1 # homeassistant.components.cloud -hass-nabucasa==0.110.1 +hass-nabucasa==0.111.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f255774e2cc64..c3acfa12affe6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ habiticalib==0.4.1 habluetooth==4.0.1 # homeassistant.components.cloud -hass-nabucasa==0.110.1 +hass-nabucasa==0.111.0 # homeassistant.components.assist_satellite # homeassistant.components.conversation From 164e5871cbae4c0e40662a67868963d061014c89 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 5 Aug 2025 09:08:33 +0200 Subject: [PATCH 085/247] Bump deebot-client to 13.6.0 (#149983) --- 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 ceb7a1da9de91..ddd464bdc6a57 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"] + "requirements": ["py-sucks==0.9.11", "deebot-client==13.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 12dfc25e16174..a6530ec0ce81e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -777,7 +777,7 @@ decora-wifi==1.4 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.5.0 +deebot-client==13.6.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3acfa12affe6..1a2634ff4487d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -677,7 +677,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.5.0 +deebot-client==13.6.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 67ecea07786eadf35fbf8e8928c2165cabc67551 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:54:50 -0400 Subject: [PATCH 086/247] Create battery_level deprecation repair for template vacuum platform (#149987) Co-authored-by: Norbert Rittel --- .../components/template/strings.json | 6 +++ homeassistant/components/template/vacuum.py | 47 ++++++++++++++++++- tests/components/template/test_vacuum.py | 33 ++++++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index be5fb1866ea6d..96c8435c25cd2 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -440,6 +440,12 @@ } } }, + "issues": { + "deprecated_battery_level": { + "title": "Deprecated battery level option in {entity_name}", + "description": "The template vacuum options `battery_level` and `battery_level_template` are being removed in 2026.8.\n\nPlease remove the `battery_level` or `battery_level_template` option from the YAML configuration for {entity_id} ({entity_name})." + } + }, "options": { "step": { "alarm_control_panel": { diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 1abfdbd00dabb..242a534187a64 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -34,11 +34,16 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import ( + config_validation as cv, + issue_registry as ir, + template, +) from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, AddEntitiesCallback, ) +from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN @@ -188,6 +193,26 @@ def async_create_preview_vacuum( ) +def create_issue( + hass: HomeAssistant, supported_features: int, name: str, entity_id: str +) -> None: + """Create the battery_level issue.""" + if supported_features & VacuumEntityFeature.BATTERY: + key = "deprecated_battery_level" + ir.async_create_issue( + hass, + DOMAIN, + f"{key}_{entity_id}", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=key, + translation_placeholders={ + "entity_name": name, + "entity_id": entity_id, + }, + ) + + class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity): """Representation of a template vacuum features.""" @@ -369,6 +394,16 @@ def __init__( self.add_script(action_id, action_config, name, DOMAIN) self._attr_supported_features |= supported_feature + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + create_issue( + self.hass, + self._attr_supported_features, + self._attr_name or DEFAULT_NAME, + self.entity_id, + ) + @callback def _async_setup_templates(self) -> None: """Set up templates.""" @@ -434,6 +469,16 @@ def __init__( self._to_render_simple.append(key) self._parse_result.add(key) + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + create_issue( + self.hass, + self._attr_supported_features, + self._attr_name or DEFAULT_NAME, + self.entity_id, + ) + @callback def _handle_coordinator_update(self) -> None: """Handle update of the data.""" diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 6c7222645b624..d0e6488e46e62 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -15,7 +15,7 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component @@ -589,6 +589,37 @@ async def test_battery_level_template( _verify(hass, STATE_UNKNOWN, expected) +@pytest.mark.parametrize( + ("count", "state_template", "extra_config", "attribute_template"), + [(1, "{{ states('sensor.test_state') }}", {}, "{{ 50 }}")], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "battery_level_template"), + (ConfigurationStyle.MODERN, "battery_level"), + (ConfigurationStyle.TRIGGER, "battery_level"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") +async def test_battery_level_template_repair( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test battery_level template raises issue.""" + # Ensure trigger entity templates are rendered + hass.states.async_set(TEST_STATE_SENSOR, VacuumActivity.DOCKED) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + "template", f"deprecated_battery_level_{TEST_ENTITY_ID}" + ) + assert issue.domain == "template" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders["entity_name"] == TEST_OBJECT_ID + assert issue.translation_placeholders["entity_id"] == TEST_ENTITY_ID + + @pytest.mark.parametrize( ("count", "state_template", "extra_config"), [ From 74c25496bc882e4db17b33270efa30333f6d9d32 Mon Sep 17 00:00:00 2001 From: Grzegorz M <13075554+grzesjam@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:09:03 +0200 Subject: [PATCH 087/247] Bump icalendar from 6.1.0 to 6.3.1 for CalDav (#149990) --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index d0e0bd0b1d04e..3b201c79e0cbb 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/caldav", "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"], - "requirements": ["caldav==1.6.0", "icalendar==6.1.0"] + "requirements": ["caldav==1.6.0", "icalendar==6.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a6530ec0ce81e..fad792d0267a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ ibmiotf==0.3.4 ical==11.0.0 # homeassistant.components.caldav -icalendar==6.1.0 +icalendar==6.3.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a2634ff4487d..a7a99823a9a91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1056,7 +1056,7 @@ ibeacon-ble==1.2.0 ical==11.0.0 # homeassistant.components.caldav -icalendar==6.1.0 +icalendar==6.3.1 # homeassistant.components.ping icmplib==3.0 From d20302f97b477d8ec021e9290e127c6bde5d0796 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 5 Aug 2025 09:03:23 +0200 Subject: [PATCH 088/247] Update knx-frontend to 2025.8.4.154919 (#149991) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 6a4565dde0ea1..f40fa028e88ec 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.8.0", "xknxproject==3.8.2", - "knx-frontend==2025.7.23.50952" + "knx-frontend==2025.8.4.154919" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index fad792d0267a7..35f9240b7785c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ kiwiki-client==0.1.1 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.7.23.50952 +knx-frontend==2025.8.4.154919 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7a99823a9a91..cd4f8bcdb91b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1129,7 +1129,7 @@ kegtron-ble==0.4.0 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.7.23.50952 +knx-frontend==2025.8.4.154919 # homeassistant.components.konnected konnected==1.2.0 From faf0ded854fb4a115f602766527eb21b9aa6e0e5 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 5 Aug 2025 08:48:47 +0200 Subject: [PATCH 089/247] Bump aioautomower to 2.1.2 (#150003) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index a0f25b1df4cac..49eb364858fa9 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["aioautomower"], "quality_scale": "silver", - "requirements": ["aioautomower==2.1.1"] + "requirements": ["aioautomower==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 35f9240b7785c..bd7c2a935b822 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2.1.1 +aioautomower==2.1.2 # homeassistant.components.azure_devops aioazuredevops==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd4f8bcdb91b2..deef9f0987cb7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -192,7 +192,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2.1.1 +aioautomower==2.1.2 # homeassistant.components.azure_devops aioazuredevops==2.2.1 From 8f5bd51eef30398fc9c52c3d390bc2bc263ccffa Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 4 Aug 2025 16:36:24 -0500 Subject: [PATCH 090/247] Bump wyoming to 1.7.2 (#150007) --- homeassistant/components/wyoming/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wyoming/manifest.json b/homeassistant/components/wyoming/manifest.json index 31adb17d7f5bb..39f5267006eb4 100644 --- a/homeassistant/components/wyoming/manifest.json +++ b/homeassistant/components/wyoming/manifest.json @@ -13,6 +13,6 @@ "documentation": "https://www.home-assistant.io/integrations/wyoming", "integration_type": "service", "iot_class": "local_push", - "requirements": ["wyoming==1.7.1"], + "requirements": ["wyoming==1.7.2"], "zeroconf": ["_wyoming._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index bd7c2a935b822..f52a9d5b85caa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3133,7 +3133,7 @@ wolf-comm==0.0.23 wsdot==0.0.1 # homeassistant.components.wyoming -wyoming==1.7.1 +wyoming==1.7.2 # homeassistant.components.xbox xbox-webapi==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index deef9f0987cb7..a7c9d5fc60fa4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2586,7 +2586,7 @@ wolf-comm==0.0.23 wsdot==0.0.1 # homeassistant.components.wyoming -wyoming==1.7.1 +wyoming==1.7.2 # homeassistant.components.xbox xbox-webapi==2.1.0 From 094fe435576d1b23844f3645bda1b3396302fb01 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 5 Aug 2025 08:50:42 +0200 Subject: [PATCH 091/247] Fix Z-Wave duplicate provisioned device (#150008) --- homeassistant/components/zwave_js/__init__.py | 58 +++++++++-------- tests/components/zwave_js/test_init.py | 64 ++++++++++++++++--- 2 files changed, 88 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 923cd776f9241..af42f024e6ac0 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -509,7 +509,7 @@ async def async_on_node_added(self, node: ZwaveNode) -> None: ) ) - await self.async_check_preprovisioned_device(node) + await self.async_check_pre_provisioned_device(node) if node.is_controller_node: # Create a controller status sensor for each device @@ -637,8 +637,8 @@ def async_on_identify(self, event: dict) -> None: f"{DOMAIN}.identify_controller.{dev_id[1]}", ) - async def async_check_preprovisioned_device(self, node: ZwaveNode) -> None: - """Check if the node was preprovisioned and update the device registry.""" + async def async_check_pre_provisioned_device(self, node: ZwaveNode) -> None: + """Check if the node was pre-provisioned and update the device registry.""" provisioning_entry = ( await self.driver_events.driver.controller.async_get_provisioning_entry( node.node_id @@ -648,29 +648,37 @@ async def async_check_preprovisioned_device(self, node: ZwaveNode) -> None: provisioning_entry and provisioning_entry.additional_properties and "device_id" in provisioning_entry.additional_properties - ): - preprovisioned_device = self.dev_reg.async_get( - provisioning_entry.additional_properties["device_id"] + and ( + pre_provisioned_device := self.dev_reg.async_get( + provisioning_entry.additional_properties["device_id"] + ) ) - - if preprovisioned_device: - dsk = provisioning_entry.dsk - dsk_identifier = (DOMAIN, f"provision_{dsk}") - - # If the pre-provisioned device has the DSK identifier, remove it - if dsk_identifier in preprovisioned_device.identifiers: - driver = self.driver_events.driver - device_id = get_device_id(driver, node) - device_id_ext = get_device_id_ext(driver, node) - new_identifiers = preprovisioned_device.identifiers.copy() - new_identifiers.remove(dsk_identifier) - new_identifiers.add(device_id) - if device_id_ext: - new_identifiers.add(device_id_ext) - self.dev_reg.async_update_device( - preprovisioned_device.id, - new_identifiers=new_identifiers, - ) + and (dsk_identifier := (DOMAIN, f"provision_{provisioning_entry.dsk}")) + in pre_provisioned_device.identifiers + ): + driver = self.driver_events.driver + device_id = get_device_id(driver, node) + device_id_ext = get_device_id_ext(driver, node) + new_identifiers = pre_provisioned_device.identifiers.copy() + new_identifiers.remove(dsk_identifier) + new_identifiers.add(device_id) + if device_id_ext: + new_identifiers.add(device_id_ext) + + if self.dev_reg.async_get_device(identifiers=new_identifiers): + # If a device entry is registered with the node ID based identifiers, + # just remove the device entry with the DSK identifier. + self.dev_reg.async_update_device( + pre_provisioned_device.id, + remove_config_entry_id=self.config_entry.entry_id, + ) + else: + # Add the node ID based identifiers to the device entry + # with the DSK identifier and remove the DSK identifier. + self.dev_reg.async_update_device( + pre_provisioned_device.id, + new_identifiers=new_identifiers, + ) async def async_register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry: """Register node in dev reg.""" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 3c39868ff9314..1aaa9013d8731 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -497,17 +497,17 @@ async def test_on_node_added_ready( ) -async def test_on_node_added_preprovisioned( +async def test_check_pre_provisioned_device_update_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - multisensor_6_state, - client, - integration, + multisensor_6_state: NodeDataType, + client: MagicMock, + integration: MockConfigEntry, ) -> None: - """Test node added event with a preprovisioned device.""" + """Test check pre-provisioned device that should update the device.""" dsk = "test" node = Node(client, deepcopy(multisensor_6_state)) - device = device_registry.async_get_or_create( + pre_provisioned_device = device_registry.async_get_or_create( config_entry_id=integration.entry_id, identifiers={(DOMAIN, f"provision_{dsk}")}, ) @@ -515,7 +515,7 @@ async def test_on_node_added_preprovisioned( { "dsk": dsk, "securityClasses": [SecurityClass.S2_UNAUTHENTICATED], - "device_id": device.id, + "device_id": pre_provisioned_device.id, } ) with patch( @@ -526,14 +526,60 @@ async def test_on_node_added_preprovisioned( client.driver.controller.emit("node added", event) await hass.async_block_till_done() - device = device_registry.async_get(device.id) + device = device_registry.async_get(pre_provisioned_device.id) assert device assert device.identifiers == { get_device_id(client.driver, node), get_device_id_ext(client.driver, node), } assert device.sw_version == node.firmware_version - # There should only be the controller and the preprovisioned device + # There should only be the controller and the pre-provisioned device + assert len(device_registry.devices) == 2 + + +async def test_check_pre_provisioned_device_remove_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + multisensor_6_state: NodeDataType, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test check pre-provisioned device that should remove the device.""" + dsk = "test" + driver = client.driver + node = Node(client, deepcopy(multisensor_6_state)) + pre_provisioned_device = device_registry.async_get_or_create( + config_entry_id=integration.entry_id, + identifiers={(DOMAIN, f"provision_{dsk}")}, + ) + extended_identifier = get_device_id_ext(driver, node) + assert extended_identifier + existing_device = device_registry.async_get_or_create( + config_entry_id=integration.entry_id, + identifiers={ + get_device_id(driver, node), + extended_identifier, + }, + ) + provisioning_entry = ProvisioningEntry.from_dict( + { + "dsk": dsk, + "securityClasses": [SecurityClass.S2_UNAUTHENTICATED], + "device_id": pre_provisioned_device.id, + } + ) + with patch( + f"{CONTROLLER_PATCH_PREFIX}.async_get_provisioning_entry", + side_effect=lambda id: provisioning_entry if id == node.node_id else None, + ): + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + assert not device_registry.async_get(pre_provisioned_device.id) + assert device_registry.async_get(existing_device.id) + + # There should only be the controller and the existing device assert len(device_registry.devices) == 2 From 808273962d7d85361bc3dbf60d0e452a08bd5f09 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Aug 2025 12:01:54 +0000 Subject: [PATCH 092/247] Bump version to 2025.8.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 85210a5456af1..b6f254e50eb71 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 457caf054dee4..160d7e042090f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0b2" +version = "2025.8.0b3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 55301a50b24902924845db541defecded2f98dda Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 6 Aug 2025 02:32:42 -0700 Subject: [PATCH 093/247] Fix PG&E and Duquesne Light Company in Opower (#149658) Co-authored-by: Norbert Rittel --- .../components/opower/config_flow.py | 216 ++++++---- homeassistant/components/opower/const.py | 1 + .../components/opower/coordinator.py | 7 +- homeassistant/components/opower/manifest.json | 2 +- homeassistant/components/opower/strings.json | 49 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/opower/test_config_flow.py | 408 +++++++++++++++--- 8 files changed, 538 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index e7f2534e1adb3..b66c4c6870e5d 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -9,6 +9,8 @@ from opower import ( CannotConnect, InvalidAuth, + MfaChallenge, + MfaHandlerBase, Opower, create_cookie_jar, get_supported_utility_names, @@ -16,49 +18,34 @@ ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.typing import VolDictType -from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN +from .const import CONF_LOGIN_DATA, CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN _LOGGER = logging.getLogger(__name__) - -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()), - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - } -) +CONF_MFA_CODE = "mfa_code" +CONF_MFA_METHOD = "mfa_method" async def _validate_login( - hass: HomeAssistant, login_data: dict[str, str] -) -> dict[str, str]: - """Validate login data and return any errors.""" + hass: HomeAssistant, + data: Mapping[str, Any], +) -> None: + """Validate login data and raise exceptions on failure.""" api = Opower( async_create_clientsession(hass, cookie_jar=create_cookie_jar()), - login_data[CONF_UTILITY], - login_data[CONF_USERNAME], - login_data[CONF_PASSWORD], - login_data.get(CONF_TOTP_SECRET), + data[CONF_UTILITY], + data[CONF_USERNAME], + data[CONF_PASSWORD], + data.get(CONF_TOTP_SECRET), + data.get(CONF_LOGIN_DATA), ) - errors: dict[str, str] = {} - try: - await api.async_login() - except InvalidAuth: - _LOGGER.exception( - "Invalid auth when connecting to %s", login_data[CONF_UTILITY] - ) - errors["base"] = "invalid_auth" - except CannotConnect: - _LOGGER.exception("Could not connect to %s", login_data[CONF_UTILITY]) - errors["base"] = "cannot_connect" - return errors + await api.async_login() class OpowerConfigFlow(ConfigFlow, domain=DOMAIN): @@ -68,81 +55,147 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize a new OpowerConfigFlow.""" - self.utility_info: dict[str, Any] | None = None + self._data: dict[str, Any] = {} + self.mfa_handler: MfaHandlerBase | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle the initial step.""" + """Handle the initial step (select utility).""" + if user_input is not None: + self._data[CONF_UTILITY] = user_input[CONF_UTILITY] + return await self.async_step_credentials() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names())} + ), + ) + + async def async_step_credentials( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle credentials step.""" errors: dict[str, str] = {} + utility = select_utility(self._data[CONF_UTILITY]) + if user_input is not None: + self._data.update(user_input) + self._async_abort_entries_match( { - CONF_UTILITY: user_input[CONF_UTILITY], - CONF_USERNAME: user_input[CONF_USERNAME], + CONF_UTILITY: self._data[CONF_UTILITY], + CONF_USERNAME: self._data[CONF_USERNAME], } ) - if select_utility(user_input[CONF_UTILITY]).accepts_mfa(): - self.utility_info = user_input - return await self.async_step_mfa() - errors = await _validate_login(self.hass, user_input) - if not errors: - return self._async_create_opower_entry(user_input) - else: - user_input = {} - user_input.pop(CONF_PASSWORD, None) + try: + await _validate_login(self.hass, self._data) + except MfaChallenge as exc: + self.mfa_handler = exc.handler + return await self.async_step_mfa_options() + except InvalidAuth: + errors["base"] = "invalid_auth" + except CannotConnect: + errors["base"] = "cannot_connect" + else: + return self._async_create_opower_entry(self._data) + + schema_dict: VolDictType = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + if utility.accepts_totp_secret(): + schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str + return self.async_show_form( - step_id="user", + step_id="credentials", data_schema=self.add_suggested_values_to_schema( - STEP_USER_DATA_SCHEMA, user_input + vol.Schema(schema_dict), user_input ), errors=errors, ) - async def async_step_mfa( + async def async_step_mfa_options( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle MFA step.""" - assert self.utility_info is not None + """Handle MFA options step.""" errors: dict[str, str] = {} + assert self.mfa_handler is not None + if user_input is not None: - data = {**self.utility_info, **user_input} - errors = await _validate_login(self.hass, data) - if not errors: - return self._async_create_opower_entry(data) - - if errors: - schema = { - vol.Required( - CONF_USERNAME, default=self.utility_info[CONF_USERNAME] - ): str, - vol.Required(CONF_PASSWORD): str, - } - else: - schema = {} + method = user_input[CONF_MFA_METHOD] + try: + await self.mfa_handler.async_select_mfa_option(method) + except CannotConnect: + errors["base"] = "cannot_connect" + else: + return await self.async_step_mfa_code() + + mfa_options = await self.mfa_handler.async_get_mfa_options() + if not mfa_options: + return await self.async_step_mfa_code() + return self.async_show_form( + step_id="mfa_options", + data_schema=self.add_suggested_values_to_schema( + vol.Schema({vol.Required(CONF_MFA_METHOD): vol.In(mfa_options)}), + user_input, + ), + errors=errors, + ) - schema[vol.Required(CONF_TOTP_SECRET)] = str + async def async_step_mfa_code( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle MFA code submission step.""" + assert self.mfa_handler is not None + errors: dict[str, str] = {} + if user_input is not None: + code = user_input[CONF_MFA_CODE] + try: + login_data = await self.mfa_handler.async_submit_mfa_code(code) + except InvalidAuth: + errors["base"] = "invalid_mfa_code" + except CannotConnect: + errors["base"] = "cannot_connect" + else: + self._data[CONF_LOGIN_DATA] = login_data + if self.source == SOURCE_REAUTH: + return self.async_update_reload_and_abort( + self._get_reauth_entry(), data=self._data + ) + return self._async_create_opower_entry(self._data) return self.async_show_form( - step_id="mfa", - data_schema=vol.Schema(schema), + step_id="mfa_code", + data_schema=self.add_suggested_values_to_schema( + vol.Schema({vol.Required(CONF_MFA_CODE): str}), user_input + ), errors=errors, ) @callback - def _async_create_opower_entry(self, data: dict[str, Any]) -> ConfigFlowResult: + def _async_create_opower_entry( + self, data: dict[str, Any], **kwargs: Any + ) -> ConfigFlowResult: """Create the config entry.""" return self.async_create_entry( title=f"{data[CONF_UTILITY]} ({data[CONF_USERNAME]})", data=data, + **kwargs, ) async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Handle configuration by re-auth.""" - return await self.async_step_reauth_confirm() + reauth_entry = self._get_reauth_entry() + self._data = dict(reauth_entry.data) + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={CONF_NAME: reauth_entry.title}, + ) async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None @@ -150,21 +203,34 @@ async def async_step_reauth_confirm( """Dialog that informs the user that reauth is required.""" errors: dict[str, str] = {} reauth_entry = self._get_reauth_entry() + if user_input is not None: - data = {**reauth_entry.data, **user_input} - errors = await _validate_login(self.hass, data) - if not errors: - return self.async_update_reload_and_abort(reauth_entry, data=data) + self._data.update(user_input) + try: + await _validate_login(self.hass, self._data) + except MfaChallenge as exc: + self.mfa_handler = exc.handler + return await self.async_step_mfa_options() + except InvalidAuth: + errors["base"] = "invalid_auth" + except CannotConnect: + errors["base"] = "cannot_connect" + else: + return self.async_update_reload_and_abort(reauth_entry, data=self._data) - schema: VolDictType = { - vol.Required(CONF_USERNAME): reauth_entry.data[CONF_USERNAME], + utility = select_utility(self._data[CONF_UTILITY]) + schema_dict: VolDictType = { + vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } - if select_utility(reauth_entry.data[CONF_UTILITY]).accepts_mfa(): - schema[vol.Optional(CONF_TOTP_SECRET)] = str + if utility.accepts_totp_secret(): + schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str + return self.async_show_form( step_id="reauth_confirm", - data_schema=vol.Schema(schema), + data_schema=self.add_suggested_values_to_schema( + vol.Schema(schema_dict), self._data + ), errors=errors, description_placeholders={CONF_NAME: reauth_entry.title}, ) diff --git a/homeassistant/components/opower/const.py b/homeassistant/components/opower/const.py index c07d41bbdcf76..5da50b2b06f29 100644 --- a/homeassistant/components/opower/const.py +++ b/homeassistant/components/opower/const.py @@ -4,3 +4,4 @@ CONF_UTILITY = "utility" CONF_TOTP_SECRET = "totp_secret" +CONF_LOGIN_DATA = "login_data" diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 189fa185cd17e..e6fbbee0bb634 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -14,7 +14,7 @@ ReadResolution, create_cookie_jar, ) -from opower.exceptions import ApiException, CannotConnect, InvalidAuth +from opower.exceptions import ApiException, CannotConnect, InvalidAuth, MfaChallenge from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import ( @@ -36,7 +36,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util -from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN +from .const import CONF_LOGIN_DATA, CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -69,6 +69,7 @@ def __init__( config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD], config_entry.data.get(CONF_TOTP_SECRET), + config_entry.data.get(CONF_LOGIN_DATA), ) @callback @@ -90,7 +91,7 @@ async def _async_update_data( # Given the infrequent updating (every 12h) # assume previous session has expired and re-login. await self.api.async_login() - except InvalidAuth as err: + except (InvalidAuth, MfaChallenge) as err: _LOGGER.error("Error during login: %s", err) raise ConfigEntryAuthFailed from err except CannotConnect as err: diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 4e88c5a68ccaa..a10c5b2d15d4a 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.12.4"] + "requirements": ["opower==0.15.1"] } diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index 8d8cecff9057d..5bb2269922023 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -3,27 +3,43 @@ "step": { "user": { "data": { - "utility": "Utility name", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "utility": "Utility name" }, "data_description": { - "utility": "The name of your utility provider", - "username": "The username for your utility account", - "password": "The password for your utility account" + "utility": "The name of your utility provider" } }, - "mfa": { - "description": "The TOTP secret below is not one of the 6-digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.", + "credentials": { + "title": "Enter Credentials", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "totp_secret": "TOTP secret" }, "data_description": { - "username": "[%key:component::opower::config::step::user::data_description::username%]", - "password": "[%key:component::opower::config::step::user::data_description::password%]", - "totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)." + "username": "The username for your utility account", + "password": "The password for your utility account", + "totp_secret": "This is not a 6-digit code. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation." + } + }, + "mfa_options": { + "title": "Multi-factor authentication", + "description": "Your account requires multi-factor authentication (MFA). Select a method to receive your security code.", + "data": { + "mfa_method": "MFA method" + }, + "data_description": { + "mfa_method": "How to receive your security code" + } + }, + "mfa_code": { + "title": "Enter security code", + "description": "A security code has been sent via your selected method. Please enter it below to complete login.", + "data": { + "mfa_code": "Security code" + }, + "data_description": { + "mfa_code": "Typically a 6-digit code" } }, "reauth_confirm": { @@ -31,18 +47,19 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "totp_secret": "[%key:component::opower::config::step::mfa::data::totp_secret%]" + "totp_secret": "[%key:component::opower::config::step::credentials::data::totp_secret%]" }, "data_description": { - "username": "[%key:component::opower::config::step::user::data_description::username%]", - "password": "[%key:component::opower::config::step::user::data_description::password%]", - "totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)." + "username": "[%key:component::opower::config::step::credentials::data_description::username%]", + "password": "[%key:component::opower::config::step::credentials::data_description::password%]", + "totp_secret": "[%key:component::opower::config::step::credentials::data_description::totp_secret%]" } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_mfa_code": "The security code is incorrect. Please try again." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", diff --git a/requirements_all.txt b/requirements_all.txt index f52a9d5b85caa..0987491b9c431 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1628,7 +1628,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.12.4 +opower==0.15.1 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7c9d5fc60fa4..8aabe31e908b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1384,7 +1384,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.12.4 +opower==0.15.1 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index c9edfc6808f19..4e5c3457fa6c8 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch -from opower import CannotConnect, InvalidAuth +from opower import CannotConnect, InvalidAuth, MfaChallenge import pytest from homeassistant import config_entries @@ -43,24 +43,32 @@ async def test_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert not result["errors"] + assert result["step_id"] == "user" + # Select utility + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"utility": "Pacific Gas and Electric Company (PG&E)"}, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "credentials" + + # Enter credentials with patch( "homeassistant.components.opower.config_flow.Opower.async_login", ) as mock_login: - result2 = await hass.config_entries.flow.async_configure( + result3 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "utility": "Pacific Gas and Electric Company (PG&E)", "username": "test-username", "password": "test-password", }, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Pacific Gas and Electric Company (PG&E) (test-username)" - assert result2["data"] == { + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "Pacific Gas and Electric Company (PG&E) (test-username)" + assert result3["data"] == { "utility": "Pacific Gas and Electric Company (PG&E)", "username": "test-username", "password": "test-password", @@ -69,33 +77,33 @@ async def test_form( assert mock_login.call_count == 1 -async def test_form_with_mfa( +async def test_form_with_totp( recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: - """Test we get the form.""" + """Test we can configure a utility that accepts a TOTP secret.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert not result["errors"] + assert result["step_id"] == "user" + # Select utility result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "utility": "Consolidated Edison (ConEd)", - "username": "test-username", - "password": "test-password", - }, + {"utility": "Consolidated Edison (ConEd)"}, ) assert result2["type"] is FlowResultType.FORM - assert not result2["errors"] + assert result2["step_id"] == "credentials" + # Enter credentials with patch( "homeassistant.components.opower.config_flow.Opower.async_login", ) as mock_login: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], { + "username": "test-username", + "password": "test-password", "totp_secret": "test-totp", }, ) @@ -112,43 +120,42 @@ async def test_form_with_mfa( assert mock_login.call_count == 1 -async def test_form_with_mfa_bad_secret( +async def test_form_with_invalid_totp( recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: - """Test MFA asks for password again when validation fails.""" + """Test we handle an invalid TOTP secret.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert not result["errors"] + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "utility": "Consolidated Edison (ConEd)", - "username": "test-username", - "password": "test-password", - }, + {"utility": "Consolidated Edison (ConEd)"}, ) assert result2["type"] is FlowResultType.FORM - assert not result2["errors"] + assert result2["step_id"] == "credentials" + # Enter invalid credentials with patch( "homeassistant.components.opower.config_flow.Opower.async_login", side_effect=InvalidAuth, - ) as mock_login: + ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "totp_secret": "test-totp", + "username": "test-username", + "password": "test-password", + "totp_secret": "bad-totp", }, ) assert result3["type"] is FlowResultType.FORM - assert result3["errors"] == { - "base": "invalid_auth", - } + assert result3["errors"] == {"base": "invalid_auth"} + assert result3["step_id"] == "credentials" + # Enter valid credentials with patch( "homeassistant.components.opower.config_flow.Opower.async_login", ) as mock_login: @@ -157,7 +164,7 @@ async def test_form_with_mfa_bad_secret( { "username": "test-username", "password": "updated-password", - "totp_secret": "updated-totp", + "totp_secret": "good-totp", }, ) @@ -167,26 +174,195 @@ async def test_form_with_mfa_bad_secret( "utility": "Consolidated Edison (ConEd)", "username": "test-username", "password": "updated-password", - "totp_secret": "updated-totp", + "totp_secret": "good-totp", } assert len(mock_setup_entry.mock_calls) == 1 assert mock_login.call_count == 1 +async def test_form_with_mfa_challenge( + recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test the full interactive MFA flow, including error recovery.""" + # 1. Start the flow and get to the credentials step + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"utility": "Pacific Gas and Electric Company (PG&E)"}, + ) + + # 2. Trigger an MfaChallenge on login + mock_mfa_handler = AsyncMock() + mock_mfa_handler.async_get_mfa_options.return_value = { + "Email": "fooxxx@mail.com", + "Phone": "xxx-123", + } + mock_mfa_handler.async_submit_mfa_code.return_value = { + "login_data_mock_key": "login_data_mock_value" + } + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=MfaChallenge(message="", handler=mock_mfa_handler), + ) as mock_login: + result_challenge = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + mock_login.assert_awaited_once() + + # 3. Handle the MFA options step, starting with a connection error + assert result_challenge["type"] is FlowResultType.FORM + assert result_challenge["step_id"] == "mfa_options" + mock_mfa_handler.async_get_mfa_options.assert_awaited_once() + + # Test CannotConnect on selecting MFA method + mock_mfa_handler.async_select_mfa_option.side_effect = CannotConnect + result_mfa_connect_fail = await hass.config_entries.flow.async_configure( + result["flow_id"], {"mfa_method": "Email"} + ) + mock_mfa_handler.async_select_mfa_option.assert_awaited_once_with("Email") + assert result_mfa_connect_fail["type"] is FlowResultType.FORM + assert result_mfa_connect_fail["step_id"] == "mfa_options" + assert result_mfa_connect_fail["errors"] == {"base": "cannot_connect"} + + # Retry selecting MFA method successfully + mock_mfa_handler.async_select_mfa_option.side_effect = None + result_mfa_select_ok = await hass.config_entries.flow.async_configure( + result["flow_id"], {"mfa_method": "Email"} + ) + assert mock_mfa_handler.async_select_mfa_option.call_count == 2 + assert result_mfa_select_ok["type"] is FlowResultType.FORM + assert result_mfa_select_ok["step_id"] == "mfa_code" + + # 4. Handle the MFA code step, testing multiple failure scenarios + # Test InvalidAuth on submitting code + mock_mfa_handler.async_submit_mfa_code.side_effect = InvalidAuth + result_mfa_invalid_code = await hass.config_entries.flow.async_configure( + result["flow_id"], {"mfa_code": "bad-code"} + ) + mock_mfa_handler.async_submit_mfa_code.assert_awaited_once_with("bad-code") + assert result_mfa_invalid_code["type"] is FlowResultType.FORM + assert result_mfa_invalid_code["step_id"] == "mfa_code" + assert result_mfa_invalid_code["errors"] == {"base": "invalid_mfa_code"} + + # Test CannotConnect on submitting code + mock_mfa_handler.async_submit_mfa_code.side_effect = CannotConnect + result_mfa_code_connect_fail = await hass.config_entries.flow.async_configure( + result["flow_id"], {"mfa_code": "good-code"} + ) + assert mock_mfa_handler.async_submit_mfa_code.call_count == 2 + mock_mfa_handler.async_submit_mfa_code.assert_called_with("good-code") + assert result_mfa_code_connect_fail["type"] is FlowResultType.FORM + assert result_mfa_code_connect_fail["step_id"] == "mfa_code" + assert result_mfa_code_connect_fail["errors"] == {"base": "cannot_connect"} + + # Retry submitting code successfully + mock_mfa_handler.async_submit_mfa_code.side_effect = None + result_final = await hass.config_entries.flow.async_configure( + result["flow_id"], {"mfa_code": "good-code"} + ) + assert mock_mfa_handler.async_submit_mfa_code.call_count == 3 + mock_mfa_handler.async_submit_mfa_code.assert_called_with("good-code") + + # 5. Verify the flow completes and creates the entry + assert result_final["type"] is FlowResultType.CREATE_ENTRY + assert ( + result_final["title"] + == "Pacific Gas and Electric Company (PG&E) (test-username)" + ) + assert result_final["data"] == { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username", + "password": "test-password", + "login_data": {"login_data_mock_key": "login_data_mock_value"}, + } + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_with_mfa_challenge_but_no_mfa_options( + recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test the full interactive MFA flow when there are no MFA options.""" + # 1. Start the flow and get to the credentials step + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"utility": "Pacific Gas and Electric Company (PG&E)"}, + ) + + # 2. Trigger an MfaChallenge on login + mock_mfa_handler = AsyncMock() + mock_mfa_handler.async_get_mfa_options.return_value = {} + mock_mfa_handler.async_submit_mfa_code.return_value = { + "login_data_mock_key": "login_data_mock_value" + } + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=MfaChallenge(message="", handler=mock_mfa_handler), + ) as mock_login: + result_challenge = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + mock_login.assert_awaited_once() + + # 3. No MFA options. Handle the MFA code step + assert result_challenge["type"] is FlowResultType.FORM + assert result_challenge["step_id"] == "mfa_code" + mock_mfa_handler.async_get_mfa_options.assert_awaited_once() + result_final = await hass.config_entries.flow.async_configure( + result["flow_id"], {"mfa_code": "good-code"} + ) + mock_mfa_handler.async_submit_mfa_code.assert_called_with("good-code") + + # 4. Verify the flow completes and creates the entry + assert result_final["type"] is FlowResultType.CREATE_ENTRY + assert ( + result_final["title"] + == "Pacific Gas and Electric Company (PG&E) (test-username)" + ) + assert result_final["data"] == { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username", + "password": "test-password", + "login_data": {"login_data_mock_key": "login_data_mock_value"}, + } + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.parametrize( ("api_exception", "expected_error"), [ - (InvalidAuth(), "invalid_auth"), - (CannotConnect(), "cannot_connect"), + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), ], ) async def test_form_exceptions( - recorder_mock: Recorder, hass: HomeAssistant, api_exception, expected_error + recorder_mock: Recorder, + hass: HomeAssistant, + api_exception: Exception, + expected_error: str, ) -> None: """Test we handle exceptions.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"utility": "Pacific Gas and Electric Company (PG&E)"}, + ) with patch( "homeassistant.components.opower.config_flow.Opower.async_login", @@ -195,7 +371,6 @@ async def test_form_exceptions( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "utility": "Pacific Gas and Electric Company (PG&E)", "username": "test-username", "password": "test-password", }, @@ -203,15 +378,10 @@ async def test_form_exceptions( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": expected_error} - # On error, the form should have the previous user input, except password, - # as suggested values. + # On error, the form should have the previous user input as suggested values. data_schema = result2["data_schema"].schema - assert ( - get_schema_suggested_value(data_schema, "utility") - == "Pacific Gas and Electric Company (PG&E)" - ) assert get_schema_suggested_value(data_schema, "username") == "test-username" - assert get_schema_suggested_value(data_schema, "password") is None + assert get_schema_suggested_value(data_schema, "password") == "test-password" assert mock_login.call_count == 1 @@ -224,6 +394,10 @@ async def test_form_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"utility": "Pacific Gas and Electric Company (PG&E)"}, + ) with patch( "homeassistant.components.opower.config_flow.Opower.async_login", @@ -231,7 +405,6 @@ async def test_form_already_configured( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "utility": "Pacific Gas and Electric Company (PG&E)", "username": "test-username", "password": "test-password", }, @@ -252,6 +425,10 @@ async def test_form_not_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"utility": "Pacific Gas and Electric Company (PG&E)"}, + ) with patch( "homeassistant.components.opower.config_flow.Opower.async_login", @@ -259,7 +436,6 @@ async def test_form_not_already_configured( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "utility": "Pacific Gas and Electric Company (PG&E)", "username": "test-username2", "password": "test-password", }, @@ -299,6 +475,16 @@ async def test_form_valid_reauth( assert result["context"]["source"] == "reauth" assert result["context"]["title_placeholders"] == {"name": mock_config_entry.title} + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=InvalidAuth, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["data_schema"].schema.keys() == { + "username", + "password", + } + with patch( "homeassistant.components.opower.config_flow.Opower.async_login", ) as mock_login: @@ -321,22 +507,23 @@ async def test_form_valid_reauth( assert mock_login.call_count == 1 -async def test_form_valid_reauth_with_mfa( +async def test_form_valid_reauth_with_totp( recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_unload_entry: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: - """Test that we can handle a valid reauth.""" - hass.config_entries.async_update_entry( - mock_config_entry, + """Test that we can handle a valid reauth for a utility with TOTP.""" + mock_config_entry = MockConfigEntry( + title="Consolidated Edison (ConEd) (test-username)", + domain=DOMAIN, data={ - **mock_config_entry.data, - # Requires MFA "utility": "Consolidated Edison (ConEd)", + "username": "test-username", + "password": "test-password", }, ) + mock_config_entry.add_to_hass(hass) mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) @@ -346,6 +533,17 @@ async def test_form_valid_reauth_with_mfa( assert len(flows) == 1 result = flows[0] + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=InvalidAuth, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["data_schema"].schema.keys() == { + "username", + "password", + "totp_secret", + } + with patch( "homeassistant.components.opower.config_flow.Opower.async_login", ) as mock_login: @@ -371,3 +569,109 @@ async def test_form_valid_reauth_with_mfa( assert len(mock_unload_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 assert mock_login.call_count == 1 + + +async def test_reauth_with_mfa_challenge( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_unload_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the full interactive MFA flow during reauth.""" + # 1. Set up the existing entry and trigger reauth + mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) + hass.config.components.add(DOMAIN) + mock_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + + # 2. Test failure before MFA challenge (InvalidAuth) + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=InvalidAuth, + ) as mock_login_fail_auth: + result_invalid_auth = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "bad-password", + }, + ) + mock_login_fail_auth.assert_awaited_once() + assert result_invalid_auth["type"] is FlowResultType.FORM + assert result_invalid_auth["step_id"] == "reauth_confirm" + assert result_invalid_auth["errors"] == {"base": "invalid_auth"} + + # 3. Test failure before MFA challenge (CannotConnect) + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=CannotConnect, + ) as mock_login_fail_connect: + result_cannot_connect = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "new-password", + }, + ) + mock_login_fail_connect.assert_awaited_once() + assert result_cannot_connect["type"] is FlowResultType.FORM + assert result_cannot_connect["step_id"] == "reauth_confirm" + assert result_cannot_connect["errors"] == {"base": "cannot_connect"} + + # 4. Trigger the MfaChallenge on the next attempt + mock_mfa_handler = AsyncMock() + mock_mfa_handler.async_get_mfa_options.return_value = { + "Email": "fooxxx@mail.com", + "Phone": "xxx-123", + } + mock_mfa_handler.async_submit_mfa_code.return_value = { + "login_data_mock_key": "login_data_mock_value" + } + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=MfaChallenge(message="", handler=mock_mfa_handler), + ) as mock_login_mfa: + result_mfa_challenge = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "new-password", + }, + ) + mock_login_mfa.assert_awaited_once() + + # 5. Handle the happy path for the MFA flow + assert result_mfa_challenge["type"] is FlowResultType.FORM + assert result_mfa_challenge["step_id"] == "mfa_options" + mock_mfa_handler.async_get_mfa_options.assert_awaited_once() + + result_mfa_code = await hass.config_entries.flow.async_configure( + result["flow_id"], {"mfa_method": "Phone"} + ) + mock_mfa_handler.async_select_mfa_option.assert_awaited_once_with("Phone") + assert result_mfa_code["type"] is FlowResultType.FORM + assert result_mfa_code["step_id"] == "mfa_code" + + result_final = await hass.config_entries.flow.async_configure( + result["flow_id"], {"mfa_code": "good-code"} + ) + mock_mfa_handler.async_submit_mfa_code.assert_awaited_once_with("good-code") + + # 6. Verify the reauth completes successfully + assert result_final["type"] is FlowResultType.ABORT + assert result_final["reason"] == "reauth_successful" + await hass.async_block_till_done() + + # Check that data was updated and the entry was reloaded + assert mock_config_entry.data["password"] == "new-password" + assert mock_config_entry.data["login_data"] == { + "login_data_mock_key": "login_data_mock_value" + } + assert len(mock_unload_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From a548e13da51a3f4b28da0b122f54092f55fa3c2b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 6 Aug 2025 11:51:31 +0200 Subject: [PATCH 094/247] Deprecate MQTT vacuum battery feature and remove it as default feature (#149877) Co-authored-by: Martin Hjelmare --- homeassistant/components/mqtt/strings.json | 4 ++ homeassistant/components/mqtt/vacuum.py | 36 +++++++++-- tests/components/mqtt/test_vacuum.py | 74 ++++++++++++++++++++-- 3 files changed, 103 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 0e248cfd2d2b9..25a5ce1c6e646 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -1,5 +1,9 @@ { "issues": { + "deprecated_vacuum_battery_feature": { + "title": "Deprecated battery feature used", + "description": "Vacuum entity {entity_id} implements the battery feature which is deprecated. This will stop working in Home Assistant 2026.2. Implement a separate entity for the battery state instead. To fix the issue, remove the `battery` feature from the configured supported features, and restart Home Assistant." + }, "invalid_platform_config": { "title": "Invalid config found for MQTT {domain} item", "description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue." diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index f1d2eb34fe185..28cc883fa9e6c 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -17,7 +17,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.json import json_dumps from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType @@ -25,11 +25,11 @@ from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC -from .entity import MqttEntity, async_setup_entity_entry_helper +from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .entity import IssueSeverity, MqttEntity, async_setup_entity_entry_helper from .models import ReceiveMessage from .schemas import MQTT_ENTITY_COMMON_SCHEMA -from .util import valid_publish_topic +from .util import learn_more_url, valid_publish_topic PARALLEL_UPDATES = 0 @@ -84,6 +84,8 @@ VacuumEntityFeature.STOP: "stop", VacuumEntityFeature.RETURN_HOME: "return_home", VacuumEntityFeature.FAN_SPEED: "fan_speed", + # Use of the battery feature was deprecated in HA Core 2025.8 + # and will be removed with HA Core 2026.2 VacuumEntityFeature.BATTERY: "battery", VacuumEntityFeature.STATUS: "status", VacuumEntityFeature.SEND_COMMAND: "send_command", @@ -96,7 +98,6 @@ VacuumEntityFeature.START | VacuumEntityFeature.STOP | VacuumEntityFeature.RETURN_HOME - | VacuumEntityFeature.BATTERY | VacuumEntityFeature.CLEAN_SPOT ) ALL_SERVICES = ( @@ -251,10 +252,35 @@ def _strings_to_services( ) } + async def mqtt_async_added_to_hass(self) -> None: + """Check for use of deprecated battery features.""" + if self.supported_features & VacuumEntityFeature.BATTERY: + ir.async_create_issue( + self.hass, + DOMAIN, + f"deprecated_vacuum_battery_feature_{self.entity_id}", + issue_domain=vacuum.DOMAIN, + breaks_in_ha_version="2026.2", + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url=learn_more_url(vacuum.DOMAIN), + translation_placeholders={"entity_id": self.entity_id}, + translation_key="deprecated_vacuum_battery_feature", + ) + _LOGGER.warning( + "MQTT vacuum entity %s implements the battery feature " + "which is deprecated. This will stop working " + "in Home Assistant 2026.2. Implement a separate entity " + "for the battery status instead", + self.entity_id, + ) + def _update_state_attributes(self, payload: dict[str, Any]) -> None: """Update the entity state attributes.""" self._state_attrs.update(payload) self._attr_fan_speed = self._state_attrs.get(FAN_SPEED, 0) + # Use of the battery feature was deprecated in HA Core 2025.8 + # and will be removed with HA Core 2026.2 self._attr_battery_level = max(0, min(100, self._state_attrs.get(BATTERY, 0))) @callback diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index ba404e2dff05c..77b90403823c3 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -32,6 +32,7 @@ from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir from .common import ( help_custom_config, @@ -108,7 +109,7 @@ async def test_default_supported_features( entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( - ["start", "stop", "return_home", "battery", "clean_spot"] + ["start", "stop", "return_home", "clean_spot"] ) @@ -313,8 +314,6 @@ async def test_status( async_fire_mqtt_message(hass, "vacuum/state", message) state = hass.states.get("vacuum.mqtttest") assert state.state == VacuumActivity.CLEANING - assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" assert state.attributes.get(ATTR_FAN_SPEED) == "max" message = """{ @@ -326,8 +325,6 @@ async def test_status( async_fire_mqtt_message(hass, "vacuum/state", message) state = hass.states.get("vacuum.mqtttest") assert state.state == VacuumActivity.DOCKED - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-60" - assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 assert state.attributes.get(ATTR_FAN_SPEED) == "min" assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ["min", "medium", "high", "max"] @@ -337,6 +334,69 @@ async def test_status( assert state.state == STATE_UNKNOWN +# Use of the battery feature was deprecated in HA Core 2025.8 +# and will be removed with HA Core 2026.2 +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + vacuum.DOMAIN, + DEFAULT_CONFIG, + ({mqttvacuum.CONF_SUPPORTED_FEATURES: ["battery"]},), + ) + ], +) +async def test_status_with_deprecated_battery_feature( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test status updates from the vacuum with deprecated battery feature.""" + await mqtt_mock_entry() + state = hass.states.get("vacuum.mqtttest") + assert state.state == STATE_UNKNOWN + + message = """{ + "battery_level": 54, + "state": "cleaning" + }""" + async_fire_mqtt_message(hass, "vacuum/state", message) + state = hass.states.get("vacuum.mqtttest") + assert state.state == VacuumActivity.CLEANING + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 + assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" + + message = """{ + "battery_level": 61, + "state": "docked" + }""" + + async_fire_mqtt_message(hass, "vacuum/state", message) + state = hass.states.get("vacuum.mqtttest") + assert state.state == VacuumActivity.DOCKED + assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-60" + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 + + message = '{"state":null}' + async_fire_mqtt_message(hass, "vacuum/state", message) + state = hass.states.get("vacuum.mqtttest") + assert state.state == STATE_UNKNOWN + assert ( + "MQTT vacuum entity vacuum.mqtttest implements " + "the battery feature which is deprecated." in caplog.text + ) + + # assert a repair issue was created for the entity + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + mqtt.DOMAIN, "deprecated_vacuum_battery_feature_vacuum.mqtttest" + ) + assert issue is not None + assert issue.issue_domain == "vacuum" + assert issue.translation_key == "deprecated_vacuum_battery_feature" + assert issue.translation_placeholders == {"entity_id": "vacuum.mqtttest"} + + @pytest.mark.parametrize( "hass_config", [ @@ -346,7 +406,9 @@ async def test_status( ( { mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( - mqttvacuum.DEFAULT_SERVICES, SERVICE_TO_STRING + mqttvacuum.DEFAULT_SERVICES + | vacuum.VacuumEntityFeature.BATTERY, + SERVICE_TO_STRING, ) }, ), From 52984f2fd16c3283eafd732c0e900a018eba1ac8 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 6 Aug 2025 10:07:36 +0200 Subject: [PATCH 095/247] Add missing translations for unhealthy Supervisor issues (#150036) --- homeassistant/components/hassio/issues.py | 6 +- homeassistant/components/hassio/strings.json | 68 +++++++++++--------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 16697659077fd..35f7f48481e2c 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -86,9 +86,11 @@ UNSUPPORTED_SKIP_REPAIR = {"privileged"} UNHEALTHY_REASONS = { "docker", - "supervisor", - "setup", + "duplicate_os_installation", + "oserror_bad_message", "privileged", + "setup", + "supervisor", "untrusted", } diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 1272b062c8bb7..97335bd5f0b8b 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -116,35 +116,43 @@ }, "unhealthy": { "title": "Unhealthy system - {reason}", - "description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this." + "description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more." }, "unhealthy_docker": { "title": "Unhealthy system - Docker misconfigured", - "description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this." + "description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more." }, - "unhealthy_supervisor": { - "title": "Unhealthy system - Supervisor update failed", - "description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this." + "unhealthy_duplicate_os_installation": { + "description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.", + "title": "Unhealthy system - Duplicate Home Assistant OS installation" }, - "unhealthy_setup": { - "title": "Unhealthy system - Setup failed", - "description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this." + "unhealthy_oserror_bad_message": { + "description": "System is currently unhealthy because the operating system has reported an OS error: Bad message. For troubleshooting information, select Learn more.", + "title": "Unhealthy system - Operating System error: Bad message" }, "unhealthy_privileged": { "title": "Unhealthy system - Not privileged", - "description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this." + "description": "System is currently unhealthy because it does not have privileged access to the docker runtime. For troubleshooting information, select Learn more." + }, + "unhealthy_setup": { + "title": "Unhealthy system - Setup failed", + "description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, For troubleshooting information, select Learn more." + }, + "unhealthy_supervisor": { + "title": "Unhealthy system - Supervisor update failed", + "description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. For troubleshooting information, select Learn more." }, "unhealthy_untrusted": { "title": "Unhealthy system - Untrusted code", - "description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this." + "description": "System is currently unhealthy because it has detected untrusted code or images in use. For troubleshooting information, select Learn more." }, "unsupported": { "title": "Unsupported system - {reason}", - "description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this." + "description": "System is unsupported due to {reason}. For troubleshooting information, select Learn more." }, "unsupported_apparmor": { "title": "Unsupported system - AppArmor issues", - "description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this." + "description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. For troubleshooting information, select Learn more." }, "unsupported_cgroup_version": { "title": "Unsupported system - CGroup version", @@ -152,23 +160,23 @@ }, "unsupported_connectivity_check": { "title": "Unsupported system - Connectivity check disabled", - "description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. Use the link to learn more and how to fix this." + "description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. For troubleshooting information, select Learn more." }, "unsupported_content_trust": { "title": "Unsupported system - Content-trust check disabled", - "description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this." + "description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. For troubleshooting information, select Learn more." }, "unsupported_dbus": { "title": "Unsupported system - D-Bus issues", - "description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this." + "description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. For troubleshooting information, select Learn more." }, "unsupported_dns_server": { "title": "Unsupported system - DNS server issues", - "description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this." + "description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. For troubleshooting information, select Learn more." }, "unsupported_docker_configuration": { "title": "Unsupported system - Docker misconfigured", - "description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this." + "description": "System is unsupported because the Docker daemon is running in an unexpected way. For troubleshooting information, select Learn more." }, "unsupported_docker_version": { "title": "Unsupported system - Docker version", @@ -176,15 +184,15 @@ }, "unsupported_job_conditions": { "title": "Unsupported system - Protections disabled", - "description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this." + "description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. For troubleshooting information, select Learn more." }, "unsupported_lxc": { "title": "Unsupported system - LXC detected", - "description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this." + "description": "System is unsupported because it is being run in an LXC virtual machine. For troubleshooting information, select Learn more." }, "unsupported_network_manager": { "title": "Unsupported system - Network Manager issues", - "description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + "description": "System is unsupported because Network Manager is missing, inactive or misconfigured. For troubleshooting information, select Learn more." }, "unsupported_os": { "title": "Unsupported system - Operating System", @@ -192,43 +200,43 @@ }, "unsupported_os_agent": { "title": "Unsupported system - OS-Agent issues", - "description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + "description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. For troubleshooting information, select Learn more." }, "unsupported_restart_policy": { "title": "Unsupported system - Container restart policy", - "description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this." + "description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. For troubleshooting information, select Learn more." }, "unsupported_software": { "title": "Unsupported system - Unsupported software", - "description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this." + "description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. For troubleshooting information, select Learn more." }, "unsupported_source_mods": { "title": "Unsupported system - Supervisor source modifications", - "description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this." + "description": "System is unsupported because Supervisor source code has been modified. For troubleshooting information, select Learn more." }, "unsupported_supervisor_version": { "title": "Unsupported system - Supervisor version", - "description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this." + "description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. For troubleshooting information, select Learn more." }, "unsupported_systemd": { "title": "Unsupported system - Systemd issues", - "description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + "description": "System is unsupported because Systemd is missing, inactive or misconfigured. For troubleshooting information, select Learn more." }, "unsupported_systemd_journal": { "title": "Unsupported system - Systemd Journal issues", - "description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + "description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. For troubleshooting information, select Learn more." }, "unsupported_systemd_resolved": { "title": "Unsupported system - Systemd-Resolved issues", - "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. For troubleshooting information, select Learn more." }, "unsupported_virtualization_image": { "title": "Unsupported system - Incorrect OS image for virtualization", - "description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this." + "description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. For troubleshooting information, select Learn more." }, "unsupported_os_version": { "title": "Unsupported system - Home Assistant OS version", - "description": "System is unsupported because the Home Assistant OS version in use is not supported. Use the link to learn more and how to fix this." + "description": "System is unsupported because the Home Assistant OS version in use is not supported. For troubleshooting information, select Learn more." } }, "entity": { From 83ccdb35f1cc31b11e02e53100602fee93e1bd3e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 5 Aug 2025 15:22:21 +0200 Subject: [PATCH 096/247] Ignore vacuum entities that properly deprecate battery (#150043) --- homeassistant/components/vacuum/__init__.py | 14 ++++++++++++-- tests/components/template/test_vacuum.py | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 4b7a6907455b5..11db9108db33a 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -79,6 +79,8 @@ _DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1") _DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1") +_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",) + class VacuumEntityFeature(IntFlag): """Supported features of the vacuum entity.""" @@ -321,7 +323,11 @@ def _report_deprecated_battery_properties(self, property: str) -> None: Integrations should implement a sensor instead. """ - if self.platform: + if ( + self.platform + and self.platform.platform_name + not in _BATTERY_DEPRECATION_IGNORED_PLATFORMS + ): # Don't report usage until after entity added to hass, after init report_usage( f"is setting the {property} which has been deprecated." @@ -341,7 +347,11 @@ def _report_deprecated_battery_feature(self) -> None: Integrations should remove the battery supported feature when migrating battery level and icon to a sensor. """ - if self.platform: + if ( + self.platform + and self.platform.platform_name + not in _BATTERY_DEPRECATION_IGNORED_PLATFORMS + ): # Don't report usage until after entity added to hass, after init report_usage( f"is setting the battery supported feature which has been deprecated." diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index d0e6488e46e62..8c2773956b294 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -603,7 +603,9 @@ async def test_battery_level_template( ) @pytest.mark.usefixtures("setup_single_attribute_state_vacuum") async def test_battery_level_template_repair( - hass: HomeAssistant, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test battery_level template raises issue.""" # Ensure trigger entity templates are rendered @@ -618,6 +620,7 @@ async def test_battery_level_template_repair( assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders["entity_name"] == TEST_OBJECT_ID assert issue.translation_placeholders["entity_id"] == TEST_ENTITY_ID + assert "Detected that integration 'template' is setting the" not in caplog.text @pytest.mark.parametrize( From e5f776fdc3a2b5ff8000ea728e9cf80c8547803e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 5 Aug 2025 16:12:55 +0200 Subject: [PATCH 097/247] Improve downloader service (#150046) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/downloader/__init__.py | 3 + .../components/downloader/services.py | 38 +++++--- .../components/downloader/strings.json | 8 ++ tests/components/downloader/conftest.py | 94 +++++++++++++++++++ tests/components/downloader/test_init.py | 66 ++++++++++--- tests/components/downloader/test_services.py | 54 +++++++++++ 6 files changed, 237 insertions(+), 26 deletions(-) create mode 100644 tests/components/downloader/conftest.py create mode 100644 tests/components/downloader/test_services.py diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index eb844ad8d3f98..8b33c1d7ed3ff 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -18,6 +18,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # If path is relative, we assume relative to Home Assistant config dir if not os.path.isabs(download_path): download_path = hass.config.path(download_path) + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path} + ) if not await hass.async_add_executor_job(os.path.isdir, download_path): _LOGGER.error( diff --git a/homeassistant/components/downloader/services.py b/homeassistant/components/downloader/services.py index bb1b968dd99e8..0ccaee232d73c 100644 --- a/homeassistant/components/downloader/services.py +++ b/homeassistant/components/downloader/services.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_register_admin_service from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path @@ -34,24 +35,33 @@ def download_file(service: ServiceCall) -> None: entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0] download_path = entry.data[CONF_DOWNLOAD_DIR] + url: str = service.data[ATTR_URL] + subdir: str | None = service.data.get(ATTR_SUBDIR) + target_filename: str | None = service.data.get(ATTR_FILENAME) + overwrite: bool = service.data[ATTR_OVERWRITE] + + if subdir: + # Check the path + try: + raise_if_invalid_path(subdir) + except ValueError as err: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="subdir_invalid", + translation_placeholders={"subdir": subdir}, + ) from err + if os.path.isabs(subdir): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="subdir_not_relative", + translation_placeholders={"subdir": subdir}, + ) def do_download() -> None: """Download the file.""" + final_path = None + filename = target_filename try: - url = service.data[ATTR_URL] - - subdir = service.data.get(ATTR_SUBDIR) - - filename = service.data.get(ATTR_FILENAME) - - overwrite = service.data.get(ATTR_OVERWRITE) - - if subdir: - # Check the path - raise_if_invalid_path(subdir) - - final_path = None - req = requests.get(url, stream=True, timeout=10) if req.status_code != HTTPStatus.OK: diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index 7db7ea459d756..98c4a0a6c82e3 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -12,6 +12,14 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "exceptions": { + "subdir_invalid": { + "message": "Invalid subdirectory, got: {subdir}" + }, + "subdir_not_relative": { + "message": "Subdirectory must be relative, got: {subdir}" + } + }, "services": { "download_file": { "name": "Download file", diff --git a/tests/components/downloader/conftest.py b/tests/components/downloader/conftest.py new file mode 100644 index 0000000000000..3bb63455ccc2d --- /dev/null +++ b/tests/components/downloader/conftest.py @@ -0,0 +1,94 @@ +"""Provide common fixtures for downloader tests.""" + +import asyncio +from pathlib import Path + +import pytest +from requests_mock import Mocker + +from homeassistant.components.downloader.const import ( + CONF_DOWNLOAD_DIR, + DOMAIN, + DOWNLOAD_COMPLETED_EVENT, + DOWNLOAD_FAILED_EVENT, +) +from homeassistant.core import Event, HomeAssistant, callback + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def setup_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> MockConfigEntry: + """Set up the downloader integration for testing.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry + + +@pytest.fixture +def mock_config_entry( + hass: HomeAssistant, + download_dir: Path, +) -> MockConfigEntry: + """Return a mocked config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_DOWNLOAD_DIR: str(download_dir)}, + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.fixture +def download_dir(tmp_path: Path) -> Path: + """Return a download directory.""" + return tmp_path + + +@pytest.fixture(autouse=True) +def mock_download_request( + requests_mock: Mocker, + download_url: str, +) -> None: + """Mock the download request.""" + requests_mock.get(download_url, text="{'one': 1}") + + +@pytest.fixture +def download_url() -> str: + """Return a mock download URL.""" + return "http://example.com/file.txt" + + +@pytest.fixture +def download_completed(hass: HomeAssistant) -> asyncio.Event: + """Return an asyncio event to wait for download completion.""" + download_event = asyncio.Event() + + @callback + def download_set(event: Event[dict[str, str]]) -> None: + """Set the event when download is completed.""" + download_event.set() + + hass.bus.async_listen_once(f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}", download_set) + + return download_event + + +@pytest.fixture +def download_failed(hass: HomeAssistant) -> asyncio.Event: + """Return an asyncio event to wait for download failure.""" + download_event = asyncio.Event() + + @callback + def download_set(event: Event[dict[str, str]]) -> None: + """Set the event when download has failed.""" + download_event.set() + + hass.bus.async_listen_once(f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", download_set) + + return download_event diff --git a/tests/components/downloader/test_init.py b/tests/components/downloader/test_init.py index e74eb376b3991..fe001838afea6 100644 --- a/tests/components/downloader/test_init.py +++ b/tests/components/downloader/test_init.py @@ -1,6 +1,8 @@ """Tests for the downloader component init.""" -from unittest.mock import patch +from pathlib import Path + +import pytest from homeassistant.components.downloader.const import ( CONF_DOWNLOAD_DIR, @@ -13,17 +15,57 @@ from tests.common import MockConfigEntry -async def test_initialization(hass: HomeAssistant) -> None: - """Test the initialization of the downloader component.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_DOWNLOAD_DIR: "/test_dir", - }, - ) - config_entry.add_to_hass(hass) - with patch("os.path.isdir", return_value=True): - assert await hass.config_entries.async_setup(config_entry.entry_id) +@pytest.fixture +def download_dir(tmp_path: Path, request: pytest.FixtureRequest) -> Path: + """Return a download directory.""" + if hasattr(request, "param"): + return tmp_path / request.param + return tmp_path + + +async def test_config_entry_setup( + hass: HomeAssistant, setup_integration: MockConfigEntry +) -> None: + """Test config entry setup.""" + config_entry = setup_integration assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) assert config_entry.state is ConfigEntryState.LOADED + + +async def test_config_entry_setup_relative_directory( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test config entry setup with a relative download directory.""" + relative_directory = "downloads" + hass.config_entries.async_update_entry( + mock_config_entry, + data={**mock_config_entry.data, CONF_DOWNLOAD_DIR: relative_directory}, + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + # The config entry will fail to set up since the directory does not exist. + # This is not relevant for this test. + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + assert mock_config_entry.data[CONF_DOWNLOAD_DIR] == hass.config.path( + relative_directory + ) + + +@pytest.mark.parametrize( + "download_dir", + [ + "not_existing_path", + ], + indirect=True, +) +async def test_config_entry_setup_not_existing_directory( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry setup without existing download directory.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert not hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/downloader/test_services.py b/tests/components/downloader/test_services.py new file mode 100644 index 0000000000000..fbdc088021aa4 --- /dev/null +++ b/tests/components/downloader/test_services.py @@ -0,0 +1,54 @@ +"""Test downloader services.""" + +import asyncio +from contextlib import AbstractContextManager, nullcontext as does_not_raise + +import pytest + +from homeassistant.components.downloader.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + + +@pytest.mark.usefixtures("setup_integration") +@pytest.mark.parametrize( + ("subdir", "expected_result"), + [ + ("test", does_not_raise()), + ("test/path", does_not_raise()), + ("~test/path", pytest.raises(ServiceValidationError)), + ("~/../test/path", pytest.raises(ServiceValidationError)), + ("../test/path", pytest.raises(ServiceValidationError)), + (".../test/path", pytest.raises(ServiceValidationError)), + ("/test/path", pytest.raises(ServiceValidationError)), + ], +) +async def test_download_invalid_subdir( + hass: HomeAssistant, + download_completed: asyncio.Event, + download_failed: asyncio.Event, + download_url: str, + subdir: str, + expected_result: AbstractContextManager, +) -> None: + """Test service invalid subdirectory.""" + + async def call_service() -> None: + """Call the download service.""" + completed = hass.async_create_task(download_completed.wait()) + failed = hass.async_create_task(download_failed.wait()) + await hass.services.async_call( + DOMAIN, + "download_file", + { + "url": download_url, + "subdir": subdir, + "filename": "file.txt", + "overwrite": True, + }, + blocking=True, + ) + await asyncio.wait((completed, failed), return_when=asyncio.FIRST_COMPLETED) + + with expected_result: + await call_service() From 7e16973166394cd758bf172293430476e89e8d63 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 5 Aug 2025 14:15:08 +0100 Subject: [PATCH 098/247] Default to zero quantity on new todo items in Mealie (#150047) --- homeassistant/components/mealie/todo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mealie/todo.py b/homeassistant/components/mealie/todo.py index e31af281783d7..c701af2865cdf 100644 --- a/homeassistant/components/mealie/todo.py +++ b/homeassistant/components/mealie/todo.py @@ -130,6 +130,7 @@ async def async_create_todo_item(self, item: TodoItem) -> None: list_id=self._shopping_list_id, note=item.summary.strip() if item.summary else item.summary, position=position, + quantity=0.0, ) try: await self.coordinator.client.add_shopping_item(new_shopping_item) From 9d806aef886d511b699c561d19c4a2ae75bd8e4a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Aug 2025 16:01:47 +0200 Subject: [PATCH 099/247] Update frontend to 20250805.0 (#150049) --- 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 706940f5da7a2..7be7dd1def9eb 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==20250731.0"] + "requirements": ["home-assistant-frontend==20250805.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b33314c0a4e2d..a0b81cd236dc7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==4.0.1 hass-nabucasa==0.111.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250731.0 +home-assistant-frontend==20250805.0 home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0987491b9c431..7d5af545302da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250731.0 +home-assistant-frontend==20250805.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8aabe31e908b9..fe2398fbcba42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250731.0 +home-assistant-frontend==20250805.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 From 20e78a15b41a375c796c06f92861ed50808f1192 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 5 Aug 2025 16:35:41 +0200 Subject: [PATCH 100/247] Change AI task strings (#150051) --- .../google_generative_ai_conversation/strings.json | 6 +++--- homeassistant/components/ollama/strings.json | 6 +++--- homeassistant/components/open_router/strings.json | 4 ++-- homeassistant/components/openai_conversation/strings.json | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/strings.json b/homeassistant/components/google_generative_ai_conversation/strings.json index 11e7c75c8ba03..545436da59037 100644 --- a/homeassistant/components/google_generative_ai_conversation/strings.json +++ b/homeassistant/components/google_generative_ai_conversation/strings.json @@ -123,10 +123,10 @@ }, "ai_task_data": { "initiate_flow": { - "user": "Add Generate data with AI service", - "reconfigure": "Reconfigure Generate data with AI service" + "user": "Add AI task", + "reconfigure": "Reconfigure AI task" }, - "entry_type": "Generate data with AI service", + "entry_type": "AI task", "step": { "set_options": { "data": { diff --git a/homeassistant/components/ollama/strings.json b/homeassistant/components/ollama/strings.json index 4f3cb3c30c0fd..9ec03cef69aab 100644 --- a/homeassistant/components/ollama/strings.json +++ b/homeassistant/components/ollama/strings.json @@ -58,10 +58,10 @@ }, "ai_task_data": { "initiate_flow": { - "user": "Add Generate data with AI service", - "reconfigure": "Reconfigure Generate data with AI service" + "user": "Add AI task", + "reconfigure": "Reconfigure AI task" }, - "entry_type": "Generate data with AI service", + "entry_type": "AI task", "step": { "set_options": { "data": { diff --git a/homeassistant/components/open_router/strings.json b/homeassistant/components/open_router/strings.json index e73a65cd178ca..43a27a91959ed 100644 --- a/homeassistant/components/open_router/strings.json +++ b/homeassistant/components/open_router/strings.json @@ -52,9 +52,9 @@ } }, "initiate_flow": { - "user": "Add Generate data with AI service" + "user": "Add AI task" }, - "entry_type": "Generate data with AI service", + "entry_type": "AI task", "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 4446eff2c9ea1..a1bf236f19b7f 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -73,10 +73,10 @@ }, "ai_task_data": { "initiate_flow": { - "user": "Add Generate data with AI service", - "reconfigure": "Reconfigure Generate data with AI service" + "user": "Add AI task", + "reconfigure": "Reconfigure AI task" }, - "entry_type": "Generate data with AI service", + "entry_type": "AI task", "step": { "init": { "data": { From e5b0a366fe1e4a19bde2aa5862419b7544bf32bf Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 5 Aug 2025 18:58:22 +0200 Subject: [PATCH 101/247] Bump reolink-aio to 0.14.6 (#150055) --- homeassistant/components/reolink/diagnostics.py | 4 ++-- homeassistant/components/reolink/manifest.json | 2 +- homeassistant/components/reolink/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/conftest.py | 2 +- tests/components/reolink/snapshots/test_diagnostics.ambr | 2 +- tests/components/reolink/test_diagnostics.py | 2 ++ tests/components/reolink/test_sensor.py | 2 +- 9 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/reolink/diagnostics.py b/homeassistant/components/reolink/diagnostics.py index 48f6b709c233d..912427fa88193 100644 --- a/homeassistant/components/reolink/diagnostics.py +++ b/homeassistant/components/reolink/diagnostics.py @@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics( IPC_cam[ch]["hardware version"] = api.camera_hardware_version(ch) IPC_cam[ch]["firmware version"] = api.camera_sw_version(ch) IPC_cam[ch]["encoding main"] = await api.get_encoding(ch) - if (signal := api.wifi_signal(ch)) is not None: + if (signal := api.wifi_signal(ch)) is not None and api.wifi_connection(ch): IPC_cam[ch]["WiFi signal"] = signal chimes: dict[int, dict[str, Any]] = {} @@ -43,7 +43,7 @@ async def async_get_config_entry_diagnostics( "HTTP(S) port": api.port, "Baichuan port": api.baichuan.port, "Baichuan only": api.baichuan_only, - "WiFi connection": api.wifi_connection, + "WiFi connection": api.wifi_connection(), "WiFi signal": api.wifi_signal(), "RTMP enabled": api.rtmp_enabled, "RTSP enabled": api.rtsp_enabled, diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index efd9f1121b690..4ad80dda8073c 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.14.5"] + "requirements": ["reolink-aio==0.14.6"] } diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index cd03f2b59b5c1..9b9a78c8ce777 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -148,7 +148,7 @@ class ReolinkHostSensorEntityDescription( native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, entity_registry_enabled_default=False, value=lambda api: api.wifi_signal(), - supported=lambda api: api.supported(None, "wifi") and api.wifi_connection, + supported=lambda api: api.supported(None, "wifi") and api.wifi_connection(), ), ReolinkHostSensorEntityDescription( key="cpu_usage", diff --git a/requirements_all.txt b/requirements_all.txt index 7d5af545302da..42fa96c525a8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2666,7 +2666,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.5 +reolink-aio==0.14.6 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe2398fbcba42..513af9c65b7df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2212,7 +2212,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.5 +reolink-aio==0.14.6 # homeassistant.components.rflink rflink==0.0.67 diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index fa4cac6fff342..48b024e0b1068 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -128,7 +128,7 @@ def _init_host_mock(host_mock: MagicMock) -> None: host_mock.session_active = True host_mock.timeout = 60 host_mock.renewtimer.return_value = 600 - host_mock.wifi_connection = False + host_mock.wifi_connection.return_value = False host_mock.wifi_signal.return_value = -45 host_mock.whiteled_mode_list.return_value = [] host_mock.post_recording_time_list.return_value = [] diff --git a/tests/components/reolink/snapshots/test_diagnostics.ambr b/tests/components/reolink/snapshots/test_diagnostics.ambr index c2b059d658bae..c43b0acdfe7b1 100644 --- a/tests/components/reolink/snapshots/test_diagnostics.ambr +++ b/tests/components/reolink/snapshots/test_diagnostics.ambr @@ -38,7 +38,7 @@ 'ONVIF enabled': True, 'RTMP enabled': True, 'RTSP enabled': True, - 'WiFi connection': False, + 'WiFi connection': True, 'WiFi signal': -45, 'abilities': dict({ 'abilityChn': list([ diff --git a/tests/components/reolink/test_diagnostics.py b/tests/components/reolink/test_diagnostics.py index b347bae9ec074..3e8ab4d0b2b95 100644 --- a/tests/components/reolink/test_diagnostics.py +++ b/tests/components/reolink/test_diagnostics.py @@ -21,6 +21,8 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test Reolink diagnostics.""" + reolink_host.wifi_connection.return_value = True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) diff --git a/tests/components/reolink/test_sensor.py b/tests/components/reolink/test_sensor.py index b30f0c2a61af1..9b32f70a9bd62 100644 --- a/tests/components/reolink/test_sensor.py +++ b/tests/components/reolink/test_sensor.py @@ -21,7 +21,7 @@ async def test_sensors( ) -> None: """Test sensor entities.""" reolink_host.ptz_pan_position.return_value = 1200 - reolink_host.wifi_connection = True + reolink_host.wifi_connection.return_value = True reolink_host.wifi_signal.return_value = -55 reolink_host.hdd_list = [0] reolink_host.hdd_storage.return_value = 95 From 80e3655bac9b42fafc9b4e95a61abbc355679895 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:56:34 -0700 Subject: [PATCH 102/247] Fix template sensor uom string (#150057) --- homeassistant/components/template/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 96c8435c25cd2..200b323d377a1 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -759,7 +759,7 @@ "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", "state": "[%key:component::template::config::step::sensor::data_description::state%]", - "unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::state%]" + "unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::unit_of_measurement%]" }, "sections": { "advanced_options": { From c8d54fcffc50a9e04a7517744e68b0766d76e181 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 5 Aug 2025 22:40:42 +0200 Subject: [PATCH 103/247] Remove matter vacuum battery level attribute (#150061) --- homeassistant/components/matter/vacuum.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/homeassistant/components/matter/vacuum.py b/homeassistant/components/matter/vacuum.py index 6ab687e060a72..cf9f26adecb4d 100644 --- a/homeassistant/components/matter/vacuum.py +++ b/homeassistant/components/matter/vacuum.py @@ -140,11 +140,6 @@ async def async_pause(self) -> None: def _update_from_device(self) -> None: """Update from device.""" self._calculate_features() - # optional battery level - if VacuumEntityFeature.BATTERY & self._attr_supported_features: - self._attr_battery_level = self.get_matter_attribute_value( - clusters.PowerSource.Attributes.BatPercentRemaining - ) # derive state from the run mode + operational state run_mode_raw: int = self.get_matter_attribute_value( clusters.RvcRunMode.Attributes.CurrentMode @@ -188,11 +183,6 @@ def _calculate_features(self) -> None: supported_features |= VacuumEntityFeature.STATE supported_features |= VacuumEntityFeature.STOP - # optional battery attribute = battery feature - if self.get_matter_attribute_value( - clusters.PowerSource.Attributes.BatPercentRemaining - ): - supported_features |= VacuumEntityFeature.BATTERY # optional identify cluster = locate feature (value must be not None or 0) if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType): supported_features |= VacuumEntityFeature.LOCATE @@ -230,7 +220,6 @@ def _calculate_features(self) -> None: clusters.RvcRunMode.Attributes.CurrentMode, clusters.RvcOperationalState.Attributes.OperationalState, ), - optional_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,), device_type=(device_types.RoboticVacuumCleaner,), allow_none_value=True, ), From baa2d751e49971a91fb5f332a14a1c48b7e27fb2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 5 Aug 2025 23:36:48 +0200 Subject: [PATCH 104/247] Bump axis to v65 (#150065) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 9758af60178db..1a12551613052 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -29,7 +29,7 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["axis"], - "requirements": ["axis==64"], + "requirements": ["axis==65"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index 42fa96c525a8a..a99ec72a12ec1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -573,7 +573,7 @@ av==13.1.0 # avion==0.10 # homeassistant.components.axis -axis==64 +axis==65 # homeassistant.components.fujitsu_fglair ayla-iot-unofficial==1.4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 513af9c65b7df..3d45d461efe07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -522,7 +522,7 @@ automower-ble==0.2.1 av==13.1.0 # homeassistant.components.axis -axis==64 +axis==65 # homeassistant.components.fujitsu_fglair ayla-iot-unofficial==1.4.7 From b370b7a7f68e63a73aaa1be7cee1c821080f08c1 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:56:27 -0400 Subject: [PATCH 105/247] Bump soco to 0.30.11 (#150072) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 5bbfc33ae5b95..79a50ef47325a 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/sonos", "iot_class": "local_push", "loggers": ["soco", "sonos_websocket"], - "requirements": ["soco==0.30.9", "sonos-websocket==0.1.3"], + "requirements": ["soco==0.30.11", "sonos-websocket==0.1.3"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/requirements_all.txt b/requirements_all.txt index a99ec72a12ec1..d2cdb33ef22a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2805,7 +2805,7 @@ smart-meter-texas==0.5.5 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.9 +soco==0.30.11 # homeassistant.components.solaredge_local solaredge-local==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d45d461efe07..64f8bdb512adb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2315,7 +2315,7 @@ smart-meter-texas==0.5.5 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.9 +soco==0.30.11 # homeassistant.components.solarlog solarlog_cli==0.4.0 From 00baecd01e9ce2aac0ac89cc5c066365bdd0114a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Aug 2025 11:55:01 -1000 Subject: [PATCH 106/247] Bump yalexs to 8.11.1 (#150073) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index e7af7d84942cc..51c5225b894c4 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"] + "requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index aa68009ac7296..9086bb1557525 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"] + "requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index d2cdb33ef22a5..8db1ebbd81e09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3167,7 +3167,7 @@ yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.10.0 +yalexs==8.11.1 # homeassistant.components.yeelight yeelight==0.7.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64f8bdb512adb..a0cac6925a1f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2617,7 +2617,7 @@ yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.10.0 +yalexs==8.11.1 # homeassistant.components.yeelight yeelight==0.7.16 From b6b422775a7bb25a4f069907fc14baad86f2b891 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Aug 2025 21:51:44 -1000 Subject: [PATCH 107/247] Bump habluetooth to 4.0.2 (#150078) Co-authored-by: Robert Resch --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index cd6aae9125921..ce5d98f8edbaa 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -21,6 +21,6 @@ "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", "dbus-fast==2.44.3", - "habluetooth==4.0.1" + "habluetooth==4.0.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a0b81cd236dc7..ddd1dd1ee662e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 -habluetooth==4.0.1 +habluetooth==4.0.2 hass-nabucasa==0.111.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 8db1ebbd81e09..13b36bafa3fdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1130,7 +1130,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.1 # homeassistant.components.bluetooth -habluetooth==4.0.1 +habluetooth==4.0.2 # homeassistant.components.cloud hass-nabucasa==0.111.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0cac6925a1f9..3b0d7f2db2ed9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.1 # homeassistant.components.bluetooth -habluetooth==4.0.1 +habluetooth==4.0.2 # homeassistant.components.cloud hass-nabucasa==0.111.0 From f3a50c176da127cd9821effdfdd2434961ba89c4 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Wed, 6 Aug 2025 13:20:37 +0800 Subject: [PATCH 108/247] Bump pyswitchbot to 0.68.3 (#150080) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 22168c21f97f1..6ed11acda082e 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -41,5 +41,5 @@ "iot_class": "local_push", "loggers": ["switchbot"], "quality_scale": "gold", - "requirements": ["PySwitchbot==0.68.2"] + "requirements": ["PySwitchbot==0.68.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 13b36bafa3fdb..ae9c98b35510b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -84,7 +84,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.68.2 +PySwitchbot==0.68.3 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b0d7f2db2ed9..7208e678dd5e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.68.2 +PySwitchbot==0.68.3 # homeassistant.components.syncthru PySyncThru==0.8.0 From 0a72f31504ef2e720033523b37812fc3f69616d9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 6 Aug 2025 03:22:07 -0400 Subject: [PATCH 109/247] Bump ZHA to 0.0.66 (#150081) --- 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 facde4ead3a1d..38ce08aa782d4 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.65"], + "requirements": ["zha==0.0.66"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index ae9c98b35510b..286776c0abc60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3203,7 +3203,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.65 +zha==0.0.66 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7208e678dd5e2..a501327cf4f03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2647,7 +2647,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.65 +zha==0.0.66 # homeassistant.components.zwave_js zwave-js-server-python==0.67.1 From a9998b41a5297a5b72f11cf68aa331c2e6ba4c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 6 Aug 2025 08:24:09 +0100 Subject: [PATCH 110/247] Bump hass-nabucasa from 0.111.0 to 0.111.1 (#150082) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 0ef407b3628a0..76e55bc19b3b8 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.111.0"], + "requirements": ["hass-nabucasa==0.111.1"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ddd1dd1ee662e..96707d39ccb9d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==4.0.2 -hass-nabucasa==0.111.0 +hass-nabucasa==0.111.1 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250805.0 diff --git a/pyproject.toml b/pyproject.toml index 160d7e042090f..5eadf909718eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.111.0", + "hass-nabucasa==0.111.1", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", diff --git a/requirements.txt b/requirements.txt index 90953842e20ce..af9a835e0d95a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.111.0 +hass-nabucasa==0.111.1 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 286776c0abc60..4f71e4bde79ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ habiticalib==0.4.1 habluetooth==4.0.2 # homeassistant.components.cloud -hass-nabucasa==0.111.0 +hass-nabucasa==0.111.1 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a501327cf4f03..4e23e5c0d6abb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ habiticalib==0.4.1 habluetooth==4.0.2 # homeassistant.components.cloud -hass-nabucasa==0.111.0 +hass-nabucasa==0.111.1 # homeassistant.components.assist_satellite # homeassistant.components.conversation From 4e21ef5fbc55756341cb720d6723855f3c01714d Mon Sep 17 00:00:00 2001 From: Philipp Waller <1090452+philippwaller@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:28:44 +0200 Subject: [PATCH 111/247] Update knx-frontend to 2025.8.6.52906 (#150085) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f40fa028e88ec..f3013de45564c 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.8.0", "xknxproject==3.8.2", - "knx-frontend==2025.8.4.154919" + "knx-frontend==2025.8.6.52906" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 4f71e4bde79ec..10ebce6309a6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ kiwiki-client==0.1.1 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.8.4.154919 +knx-frontend==2025.8.6.52906 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e23e5c0d6abb..e888bad6847e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1129,7 +1129,7 @@ kegtron-ble==0.4.0 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.8.4.154919 +knx-frontend==2025.8.6.52906 # homeassistant.components.konnected konnected==1.2.0 From d2586ca4ff126ebbd8afabf3877d3be8c2db4b29 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:02:04 +0200 Subject: [PATCH 112/247] Remove tuya vacuum battery level attribute (#150086) --- homeassistant/components/tuya/sensor.py | 7 +++++++ homeassistant/components/tuya/vacuum.py | 16 +--------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 6e8da29ef531f..ebb5c13f92a76 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -915,6 +915,13 @@ class TuyaSensorEntityDescription(SensorEntityDescription): translation_key="rolling_brush_life", state_class=SensorStateClass.MEASUREMENT, ), + TuyaSensorEntityDescription( + key=DPCode.ELECTRICITY_LEFT, + translation_key="battery", + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + ), ), # Smart Water Timer "sfkzq": ( diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index d61a624f0270a..6b4596ee0533e 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -18,7 +18,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DPCode, DPType from .entity import TuyaEntity -from .models import EnumTypeData, IntegerTypeData +from .models import EnumTypeData TUYA_MODE_RETURN_HOME = "chargego" TUYA_STATUS_TO_HA = { @@ -77,7 +77,6 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): """Tuya Vacuum Device.""" _fan_speed: EnumTypeData | None = None - _battery_level: IntegerTypeData | None = None _attr_name = None def __init__(self, device: CustomerDevice, device_manager: Manager) -> None: @@ -118,19 +117,6 @@ def __init__(self, device: CustomerDevice, device_manager: Manager) -> None: self._attr_fan_speed_list = enum_type.range self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED - if int_type := self.find_dpcode(DPCode.ELECTRICITY_LEFT, dptype=DPType.INTEGER): - self._attr_supported_features |= VacuumEntityFeature.BATTERY - self._battery_level = int_type - - @property - def battery_level(self) -> int | None: - """Return Tuya device state.""" - if self._battery_level is None or not ( - status := self.device.status.get(DPCode.ELECTRICITY_LEFT) - ): - return None - return round(self._battery_level.scale_value(status)) - @property def fan_speed(self) -> str | None: """Return the fan speed of the vacuum cleaner.""" From 47946d0103c80bb3131fe1ce2547ef5fdfa4e5e5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:23:34 +0200 Subject: [PATCH 113/247] Add Tuya debug logging for new devices (#150091) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/tuya/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 106075e9314b5..e8aa6bded220d 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -153,6 +153,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool # Register known device IDs device_registry = dr.async_get(hass) for device in manager.device_map.values(): + LOGGER.debug( + "Register device %s: %s (function: %s, status range: %s)", + device.id, + device.status, + device.function, + device.status_range, + ) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, device.id)}, @@ -237,6 +244,14 @@ def add_device(self, device: CustomerDevice) -> None: # Ensure the device isn't present stale self.hass.add_job(self.async_remove_device, device.id) + LOGGER.debug( + "Add device %s: %s (function: %s, status range: %s)", + device.id, + device.status, + device.function, + device.status_range, + ) + dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id]) def remove_device(self, device_id: str) -> None: From fa587cec38dd92bfc98238e754a61774c85d74cc Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 6 Aug 2025 10:53:55 +0200 Subject: [PATCH 114/247] Fix hassio tests by only mocking supervisor id (#150093) --- tests/components/hassio/test_config.py | 36 ++++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/tests/components/hassio/test_config.py b/tests/components/hassio/test_config.py index 4df8d2e81ac48..4cdea02b0871f 100644 --- a/tests/components/hassio/test_config.py +++ b/tests/components/hassio/test_config.py @@ -1,13 +1,16 @@ """Test websocket API.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, patch -from uuid import UUID +from uuid import UUID, uuid4 import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.auth.models import User +from homeassistant.components.hassio import HASSIO_USER_NAME from homeassistant.components.hassio.const import DATA_CONFIG_STORE, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -98,7 +101,24 @@ def mock_all( ) -@pytest.mark.usefixtures("hassio_env") +@pytest.fixture +def mock_hassio_user_id() -> Generator[None]: + """Mock the HASSIO user ID for snapshot testing.""" + original_user_init = User.__init__ + + def mock_user_init(self, *args, **kwargs): + with patch("homeassistant.auth.models.uuid.uuid4") as mock_uuid: + if kwargs.get("name") == HASSIO_USER_NAME: + mock_uuid.return_value = UUID(bytes=b"very_very_random", version=4) + else: + mock_uuid.return_value = uuid4() + original_user_init(self, *args, **kwargs) + + with patch.object(User, "__init__", mock_user_init): + yield + + +@pytest.mark.usefixtures("hassio_env", "mock_hassio_user_id") @pytest.mark.parametrize( "storage_data", [ @@ -151,10 +171,7 @@ async def test_load_config_store( await hass.auth.async_create_refresh_token(user) await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN]) - with ( - patch("homeassistant.components.hassio.config.STORE_DELAY_SAVE", 0), - patch("uuid.uuid4", return_value=UUID(bytes=b"very_very_random", version=4)), - ): + with patch("homeassistant.components.hassio.config.STORE_DELAY_SAVE", 0): assert await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() await hass.async_block_till_done() @@ -162,7 +179,7 @@ async def test_load_config_store( assert hass.data[DATA_CONFIG_STORE].data.to_dict() == snapshot -@pytest.mark.usefixtures("hassio_env") +@pytest.mark.usefixtures("hassio_env", "mock_hassio_user_id") async def test_save_config_store( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -171,10 +188,7 @@ async def test_save_config_store( snapshot: SnapshotAssertion, ) -> None: """Test saving the config store.""" - with ( - patch("homeassistant.components.hassio.config.STORE_DELAY_SAVE", 0), - patch("uuid.uuid4", return_value=UUID(bytes=b"very_very_random", version=4)), - ): + with patch("homeassistant.components.hassio.config.STORE_DELAY_SAVE", 0): assert await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() await hass.async_block_till_done() From 75200a942629efa18fb4082c32c67d5e91cc06ce Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 6 Aug 2025 10:58:52 +0200 Subject: [PATCH 115/247] Reduce Reolink fimware polling from 12h to 24h (#150095) --- homeassistant/components/reolink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 236e170746173..42a29ee6ef45c 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -59,7 +59,7 @@ Platform.UPDATE, ] DEVICE_UPDATE_INTERVAL = timedelta(seconds=60) -FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12) +FIRMWARE_UPDATE_INTERVAL = timedelta(hours=24) NUM_CRED_ERRORS = 3 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) From 1693299652dfc08a89058fa7a2d0bcf606c763bf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 6 Aug 2025 13:24:49 +0200 Subject: [PATCH 116/247] Enable disabled Anthropic config entries after entry migration (#150098) --- .../components/anthropic/__init__.py | 89 +++- .../components/anthropic/config_flow.py | 2 +- tests/components/anthropic/test_init.py | 405 +++++++++++++++++- 3 files changed, 479 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/anthropic/__init__.py b/homeassistant/components/anthropic/__init__.py index e143e4d47c22e..b996b7d38c57c 100644 --- a/homeassistant/components/anthropic/__init__.py +++ b/homeassistant/components/anthropic/__init__.py @@ -81,11 +81,15 @@ async def async_update_options( async def async_migrate_integration(hass: HomeAssistant) -> None: """Migrate integration entry structure.""" - entries = hass.config_entries.async_entries(DOMAIN) + # Make sure we get enabled config entries first + entries = sorted( + hass.config_entries.async_entries(DOMAIN), + key=lambda e: e.disabled_by is not None, + ) if not any(entry.version == 1 for entry in entries): return - api_keys_entries: dict[str, ConfigEntry] = {} + api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {} entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) @@ -99,30 +103,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: ) if entry.data[CONF_API_KEY] not in api_keys_entries: use_existing = True - api_keys_entries[entry.data[CONF_API_KEY]] = entry + all_disabled = all( + e.disabled_by is not None + for e in entries + if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY] + ) + api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled) - parent_entry = api_keys_entries[entry.data[CONF_API_KEY]] + parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]] hass.config_entries.async_add_subentry(parent_entry, subentry) - conversation_entity = entity_registry.async_get_entity_id( + conversation_entity_id = entity_registry.async_get_entity_id( "conversation", DOMAIN, entry.entry_id, ) - if conversation_entity is not None: + device = device_registry.async_get_device( + identifiers={(DOMAIN, entry.entry_id)} + ) + + if conversation_entity_id is not None: + conversation_entity_entry = entity_registry.entities[conversation_entity_id] + entity_disabled_by = conversation_entity_entry.disabled_by + if ( + entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY + and not all_disabled + ): + # Device and entity registries don't update the disabled_by flag + # when moving a device or entity from one config entry to another, + # so we need to do it manually. + entity_disabled_by = ( + er.RegistryEntryDisabler.DEVICE + if device + else er.RegistryEntryDisabler.USER + ) entity_registry.async_update_entity( - conversation_entity, + conversation_entity_id, config_entry_id=parent_entry.entry_id, config_subentry_id=subentry.subentry_id, + disabled_by=entity_disabled_by, new_unique_id=subentry.subentry_id, ) - device = device_registry.async_get_device( - identifiers={(DOMAIN, entry.entry_id)} - ) if device is not None: + # Device and entity registries don't update the disabled_by flag when + # moving a device or entity from one config entry to another, so we + # need to do it manually. + device_disabled_by = device.disabled_by + if ( + device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY + and not all_disabled + ): + device_disabled_by = dr.DeviceEntryDisabler.USER device_registry.async_update_device( device.id, + disabled_by=device_disabled_by, new_identifiers={(DOMAIN, subentry.subentry_id)}, add_config_subentry_id=subentry.subentry_id, add_config_entry_id=parent_entry.entry_id, @@ -147,7 +182,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: title=DEFAULT_CONVERSATION_NAME, options={}, version=2, - minor_version=2, + minor_version=3, ) @@ -173,6 +208,38 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) hass.config_entries.async_update_entry(entry, minor_version=2) + if entry.version == 2 and entry.minor_version == 2: + # Fix migration where the disabled_by flag was not set correctly. + # We can currently only correct this for enabled config entries, + # because migration does not run for disabled config entries. This + # is asserted in tests, and if that behavior is changed, we should + # correct also disabled config entries. + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + entity_entries = er.async_entries_for_config_entry( + entity_registry, entry.entry_id + ) + if entry.disabled_by is None: + # If the config entry is not disabled, we need to set the disabled_by + # flag on devices to USER, and on entities to DEVICE, if they are set + # to CONFIG_ENTRY. + for device in devices: + if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY: + continue + device_registry.async_update_device( + device.id, + disabled_by=dr.DeviceEntryDisabler.USER, + ) + for entity in entity_entries: + if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY: + continue + entity_registry.async_update_entity( + entity.entity_id, + disabled_by=er.RegistryEntryDisabler.DEVICE, + ) + hass.config_entries.async_update_entry(entry, minor_version=3) + LOGGER.debug( "Migration to version %s:%s successful", entry.version, entry.minor_version ) diff --git a/homeassistant/components/anthropic/config_flow.py b/homeassistant/components/anthropic/config_flow.py index 099eae73d31bb..0c555d19bd944 100644 --- a/homeassistant/components/anthropic/config_flow.py +++ b/homeassistant/components/anthropic/config_flow.py @@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Anthropic.""" VERSION = 2 - MINOR_VERSION = 2 + MINOR_VERSION = 3 async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/tests/components/anthropic/test_init.py b/tests/components/anthropic/test_init.py index be4f41ad4cd32..ff54539bb39f9 100644 --- a/tests/components/anthropic/test_init.py +++ b/tests/components/anthropic/test_init.py @@ -1,5 +1,6 @@ """Tests for the Anthropic integration.""" +from typing import Any from unittest.mock import patch from anthropic import ( @@ -12,9 +13,12 @@ import pytest from homeassistant.components.anthropic.const import DOMAIN -from homeassistant.config_entries import ConfigSubentryData +from homeassistant.config_entries import ConfigEntryDisabler, ConfigSubentryData +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntryDisabler +from homeassistant.helpers.entity_registry import RegistryEntryDisabler from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -114,7 +118,7 @@ async def test_migration_from_v1_to_v2( await hass.async_block_till_done() assert mock_config_entry.version == 2 - assert mock_config_entry.minor_version == 2 + assert mock_config_entry.minor_version == 3 assert mock_config_entry.data == {"api_key": "1234"} assert mock_config_entry.options == {} @@ -149,6 +153,207 @@ async def test_migration_from_v1_to_v2( } +@pytest.mark.parametrize( + ( + "config_entry_disabled_by", + "merged_config_entry_disabled_by", + "conversation_subentry_data", + "main_config_entry", + ), + [ + ( + [ConfigEntryDisabler.USER, None], + None, + [ + { + "conversation_entity_id": "conversation.claude_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + { + "conversation_entity_id": "conversation.claude", + "device_disabled_by": DeviceEntryDisabler.USER, + "entity_disabled_by": RegistryEntryDisabler.DEVICE, + "device": 0, + }, + ], + 1, + ), + ( + [None, ConfigEntryDisabler.USER], + None, + [ + { + "conversation_entity_id": "conversation.claude", + "device_disabled_by": DeviceEntryDisabler.USER, + "entity_disabled_by": RegistryEntryDisabler.DEVICE, + "device": 0, + }, + { + "conversation_entity_id": "conversation.claude_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + ], + 0, + ), + ( + [ConfigEntryDisabler.USER, ConfigEntryDisabler.USER], + ConfigEntryDisabler.USER, + [ + { + "conversation_entity_id": "conversation.claude", + "device_disabled_by": DeviceEntryDisabler.CONFIG_ENTRY, + "entity_disabled_by": RegistryEntryDisabler.CONFIG_ENTRY, + "device": 0, + }, + { + "conversation_entity_id": "conversation.claude_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + ], + 0, + ), + ], +) +async def test_migration_from_v1_disabled( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry_disabled_by: list[ConfigEntryDisabler | None], + merged_config_entry_disabled_by: ConfigEntryDisabler | None, + conversation_subentry_data: list[dict[str, Any]], + main_config_entry: int, +) -> None: + """Test migration where the config entries are disabled.""" + # Create a v1 config entry with conversation options and an entity + options = { + "recommended": True, + "llm_hass_api": ["assist"], + "prompt": "You are a helpful assistant", + "chat_model": "claude-3-haiku-20240307", + } + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_API_KEY: "1234"}, + options=options, + version=1, + title="Claude", + disabled_by=config_entry_disabled_by[0], + ) + mock_config_entry.add_to_hass(hass) + mock_config_entry_2 = MockConfigEntry( + domain=DOMAIN, + data={CONF_API_KEY: "1234"}, + options=options, + version=1, + title="Claude 2", + disabled_by=config_entry_disabled_by[1], + ) + mock_config_entry_2.add_to_hass(hass) + mock_config_entries = [mock_config_entry, mock_config_entry_2] + + device_1 = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, mock_config_entry.entry_id)}, + name=mock_config_entry.title, + manufacturer="Anthropic", + model="Claude", + entry_type=dr.DeviceEntryType.SERVICE, + disabled_by=DeviceEntryDisabler.CONFIG_ENTRY, + ) + entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry.entry_id, + config_entry=mock_config_entry, + device_id=device_1.id, + suggested_object_id="claude", + disabled_by=RegistryEntryDisabler.CONFIG_ENTRY, + ) + + device_2 = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_2.entry_id, + identifiers={(DOMAIN, mock_config_entry_2.entry_id)}, + name=mock_config_entry_2.title, + manufacturer="Anthropic", + model="Claude", + entry_type=dr.DeviceEntryType.SERVICE, + ) + entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry_2.entry_id, + config_entry=mock_config_entry_2, + device_id=device_2.id, + suggested_object_id="claude", + ) + + devices = [device_1, device_2] + + # Run migration + with patch( + "homeassistant.components.anthropic.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.disabled_by is merged_config_entry_disabled_by + assert entry.version == 2 + assert entry.minor_version == 3 + assert not entry.options + assert entry.title == "Claude conversation" + assert len(entry.subentries) == 2 + conversation_subentries = [ + subentry + for subentry in entry.subentries.values() + if subentry.subentry_type == "conversation" + ] + assert len(conversation_subentries) == 2 + for subentry in conversation_subentries: + assert subentry.subentry_type == "conversation" + assert subentry.data == options + assert "Claude" in subentry.title + + assert not device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + assert not device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry_2.entry_id)} + ) + + for idx, subentry in enumerate(conversation_subentries): + subentry_data = conversation_subentry_data[idx] + entity = entity_registry.async_get(subentry_data["conversation_entity_id"]) + assert entity.unique_id == subentry.subentry_id + assert entity.config_subentry_id == subentry.subentry_id + assert entity.config_entry_id == entry.entry_id + assert entity.disabled_by is subentry_data["entity_disabled_by"] + + assert ( + device := device_registry.async_get_device( + identifiers={(DOMAIN, subentry.subentry_id)} + ) + ) + assert device.identifiers == {(DOMAIN, subentry.subentry_id)} + assert device.id == devices[subentry_data["device"]].id + assert device.config_entries == { + mock_config_entries[main_config_entry].entry_id + } + assert device.config_entries_subentries == { + mock_config_entries[main_config_entry].entry_id: {subentry.subentry_id} + } + assert device.disabled_by is subentry_data["device_disabled_by"] + + async def test_migration_from_v1_to_v2_with_multiple_keys( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -226,7 +431,7 @@ async def test_migration_from_v1_to_v2_with_multiple_keys( for idx, entry in enumerate(entries): assert entry.version == 2 - assert entry.minor_version == 2 + assert entry.minor_version == 3 assert not entry.options assert len(entry.subentries) == 1 subentry = list(entry.subentries.values())[0] @@ -320,7 +525,7 @@ async def test_migration_from_v1_to_v2_with_same_keys( entry = entries[0] assert entry.version == 2 - assert entry.minor_version == 2 + assert entry.minor_version == 3 assert not entry.options assert len(entry.subentries) == 2 # Two subentries from the two original entries @@ -443,7 +648,7 @@ async def test_migration_from_v2_1_to_v2_2( assert len(entries) == 1 entry = entries[0] assert entry.version == 2 - assert entry.minor_version == 2 + assert entry.minor_version == 3 assert not entry.options assert entry.title == "Claude" assert len(entry.subentries) == 2 @@ -500,3 +705,193 @@ async def test_migration_from_v2_1_to_v2_2( assert device.config_entries_subentries == { mock_config_entry.entry_id: {subentry.subentry_id} } + + +@pytest.mark.parametrize( + ( + "config_entry_disabled_by", + "device_disabled_by", + "entity_disabled_by", + "setup_result", + "minor_version_after_migration", + "config_entry_disabled_by_after_migration", + "device_disabled_by_after_migration", + "entity_disabled_by_after_migration", + ), + [ + # Config entry not disabled, update device and entity disabled by config entry + ( + None, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + True, + 3, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + True, + 3, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + True, + 3, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + ), + ( + None, + None, + None, + True, + 3, + None, + None, + None, + ), + # Config entry disabled, migration does not run + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + False, + 2, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + ), + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + False, + 2, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + False, + 2, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + ), + ( + ConfigEntryDisabler.USER, + None, + None, + False, + 2, + ConfigEntryDisabler.USER, + None, + None, + ), + ], +) +async def test_migrate_entry_to_v2_3( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry_disabled_by: ConfigEntryDisabler | None, + device_disabled_by: DeviceEntryDisabler | None, + entity_disabled_by: RegistryEntryDisabler | None, + setup_result: bool, + minor_version_after_migration: int, + config_entry_disabled_by_after_migration: ConfigEntryDisabler | None, + device_disabled_by_after_migration: ConfigEntryDisabler | None, + entity_disabled_by_after_migration: RegistryEntryDisabler | None, +) -> None: + """Test migration to version 2.3.""" + # Create a v2.2 config entry with conversation subentries + conversation_subentry_id = "blabla" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_API_KEY: "test-api-key"}, + disabled_by=config_entry_disabled_by, + version=2, + minor_version=2, + subentries_data=[ + { + "data": { + "recommended": True, + "llm_hass_api": ["assist"], + "prompt": "You are a helpful assistant", + "chat_model": "claude-3-haiku-20240307", + }, + "subentry_id": conversation_subentry_id, + "subentry_type": "conversation", + "title": "Claude haiku", + "unique_id": None, + }, + ], + ) + mock_config_entry.add_to_hass(hass) + + conversation_device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + config_subentry_id=conversation_subentry_id, + disabled_by=device_disabled_by, + identifiers={(DOMAIN, mock_config_entry.entry_id)}, + name=mock_config_entry.title, + manufacturer="Anthropic", + model="Claude", + entry_type=dr.DeviceEntryType.SERVICE, + ) + conversation_entity = entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry.entry_id, + config_entry=mock_config_entry, + config_subentry_id=conversation_subentry_id, + disabled_by=entity_disabled_by, + device_id=conversation_device.id, + suggested_object_id="claude", + ) + + # Verify initial state + assert mock_config_entry.version == 2 + assert mock_config_entry.minor_version == 2 + assert len(mock_config_entry.subentries) == 1 + assert mock_config_entry.disabled_by == config_entry_disabled_by + assert conversation_device.disabled_by == device_disabled_by + assert conversation_entity.disabled_by == entity_disabled_by + + # Run setup to trigger migration + with patch( + "homeassistant.components.anthropic.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert result is setup_result + await hass.async_block_till_done() + + # Verify migration completed + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + + # Check version and subversion were updated + assert entry.version == 2 + assert entry.minor_version == minor_version_after_migration + + # Check the disabled_by flag on config entry, device and entity are as expected + conversation_device = device_registry.async_get(conversation_device.id) + conversation_entity = entity_registry.async_get(conversation_entity.entity_id) + assert mock_config_entry.disabled_by == config_entry_disabled_by_after_migration + assert conversation_device.disabled_by == device_disabled_by_after_migration + assert conversation_entity.disabled_by == entity_disabled_by_after_migration From 9820956b46dcdb0610f527d0d8ce95e4a101cc66 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 6 Aug 2025 13:24:37 +0200 Subject: [PATCH 117/247] Enable disabled OpenAI config entries after entry migration (#150099) --- .../openai_conversation/__init__.py | 113 ++++- .../openai_conversation/config_flow.py | 2 +- .../openai_conversation/test_init.py | 413 +++++++++++++++++- 3 files changed, 501 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 77b71ae372d83..f50563b59ea1b 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -272,11 +272,15 @@ async def async_update_options(hass: HomeAssistant, entry: OpenAIConfigEntry) -> async def async_migrate_integration(hass: HomeAssistant) -> None: """Migrate integration entry structure.""" - entries = hass.config_entries.async_entries(DOMAIN) + # Make sure we get enabled config entries first + entries = sorted( + hass.config_entries.async_entries(DOMAIN), + key=lambda e: e.disabled_by is not None, + ) if not any(entry.version == 1 for entry in entries): return - api_keys_entries: dict[str, ConfigEntry] = {} + api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {} entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) @@ -290,30 +294,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: ) if entry.data[CONF_API_KEY] not in api_keys_entries: use_existing = True - api_keys_entries[entry.data[CONF_API_KEY]] = entry + all_disabled = all( + e.disabled_by is not None + for e in entries + if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY] + ) + api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled) - parent_entry = api_keys_entries[entry.data[CONF_API_KEY]] + parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]] hass.config_entries.async_add_subentry(parent_entry, subentry) - conversation_entity = entity_registry.async_get_entity_id( + conversation_entity_id = entity_registry.async_get_entity_id( "conversation", DOMAIN, entry.entry_id, ) - if conversation_entity is not None: + device = device_registry.async_get_device( + identifiers={(DOMAIN, entry.entry_id)} + ) + + if conversation_entity_id is not None: + conversation_entity_entry = entity_registry.entities[conversation_entity_id] + entity_disabled_by = conversation_entity_entry.disabled_by + if ( + entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY + and not all_disabled + ): + # Device and entity registries don't update the disabled_by flag + # when moving a device or entity from one config entry to another, + # so we need to do it manually. + entity_disabled_by = ( + er.RegistryEntryDisabler.DEVICE + if device + else er.RegistryEntryDisabler.USER + ) entity_registry.async_update_entity( - conversation_entity, + conversation_entity_id, config_entry_id=parent_entry.entry_id, config_subentry_id=subentry.subentry_id, + disabled_by=entity_disabled_by, new_unique_id=subentry.subentry_id, ) - device = device_registry.async_get_device( - identifiers={(DOMAIN, entry.entry_id)} - ) if device is not None: + # Device and entity registries don't update the disabled_by flag when + # moving a device or entity from one config entry to another, so we + # need to do it manually. + device_disabled_by = device.disabled_by + if ( + device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY + and not all_disabled + ): + device_disabled_by = dr.DeviceEntryDisabler.USER device_registry.async_update_device( device.id, + disabled_by=device_disabled_by, new_identifiers={(DOMAIN, subentry.subentry_id)}, add_config_subentry_id=subentry.subentry_id, add_config_entry_id=parent_entry.entry_id, @@ -333,12 +368,13 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: if not use_existing: await hass.config_entries.async_remove(entry.entry_id) else: + _add_ai_task_subentry(hass, entry) hass.config_entries.async_update_entry( entry, title=DEFAULT_NAME, options={}, version=2, - minor_version=2, + minor_version=4, ) @@ -365,19 +401,56 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> hass.config_entries.async_update_entry(entry, minor_version=2) if entry.version == 2 and entry.minor_version == 2: - hass.config_entries.async_add_subentry( - entry, - ConfigSubentry( - data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS), - subentry_type="ai_task_data", - title=DEFAULT_AI_TASK_NAME, - unique_id=None, - ), - ) + _add_ai_task_subentry(hass, entry) hass.config_entries.async_update_entry(entry, minor_version=3) + if entry.version == 2 and entry.minor_version == 3: + # Fix migration where the disabled_by flag was not set correctly. + # We can currently only correct this for enabled config entries, + # because migration does not run for disabled config entries. This + # is asserted in tests, and if that behavior is changed, we should + # correct also disabled config entries. + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + entity_entries = er.async_entries_for_config_entry( + entity_registry, entry.entry_id + ) + if entry.disabled_by is None: + # If the config entry is not disabled, we need to set the disabled_by + # flag on devices to USER, and on entities to DEVICE, if they are set + # to CONFIG_ENTRY. + for device in devices: + if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY: + continue + device_registry.async_update_device( + device.id, + disabled_by=dr.DeviceEntryDisabler.USER, + ) + for entity in entity_entries: + if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY: + continue + entity_registry.async_update_entity( + entity.entity_id, + disabled_by=er.RegistryEntryDisabler.DEVICE, + ) + hass.config_entries.async_update_entry(entry, minor_version=4) + LOGGER.debug( "Migration to version %s:%s successful", entry.version, entry.minor_version ) return True + + +def _add_ai_task_subentry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> None: + """Add AI Task subentry to the config entry.""" + hass.config_entries.async_add_subentry( + entry, + ConfigSubentry( + data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS), + subentry_type="ai_task_data", + title=DEFAULT_AI_TASK_NAME, + unique_id=None, + ), + ) diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index aa1c967ca8f6a..c45c2b997b3f1 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -98,7 +98,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for OpenAI Conversation.""" VERSION = 2 - MINOR_VERSION = 3 + MINOR_VERSION = 4 async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index e728d0019b63f..fb8be3b2e6864 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -1,5 +1,6 @@ """Tests for the OpenAI integration.""" +from typing import Any from unittest.mock import AsyncMock, Mock, mock_open, patch import httpx @@ -19,12 +20,18 @@ from homeassistant.components.openai_conversation import CONF_CHAT_MODEL from homeassistant.components.openai_conversation.const import ( DEFAULT_AI_TASK_NAME, + DEFAULT_CONVERSATION_NAME, DOMAIN, + RECOMMENDED_AI_TASK_OPTIONS, + RECOMMENDED_CONVERSATION_OPTIONS, ) -from homeassistant.config_entries import ConfigSubentryData +from homeassistant.config_entries import ConfigEntryDisabler, ConfigSubentryData +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntryDisabler +from homeassistant.helpers.entity_registry import RegistryEntryDisabler from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -585,7 +592,7 @@ async def test_migration_from_v1( await hass.async_block_till_done() assert mock_config_entry.version == 2 - assert mock_config_entry.minor_version == 3 + assert mock_config_entry.minor_version == 4 assert mock_config_entry.data == {"api_key": "1234"} assert mock_config_entry.options == {} @@ -714,7 +721,7 @@ async def test_migration_from_v1_with_multiple_keys( for idx, entry in enumerate(entries): assert entry.version == 2 - assert entry.minor_version == 3 + assert entry.minor_version == 4 assert not entry.options assert len(entry.subentries) == 2 @@ -819,7 +826,7 @@ async def test_migration_from_v1_with_same_keys( entry = entries[0] assert entry.version == 2 - assert entry.minor_version == 3 + assert entry.minor_version == 4 assert not entry.options assert ( len(entry.subentries) == 3 @@ -855,6 +862,215 @@ async def test_migration_from_v1_with_same_keys( } +@pytest.mark.parametrize( + ( + "config_entry_disabled_by", + "merged_config_entry_disabled_by", + "conversation_subentry_data", + "main_config_entry", + ), + [ + ( + [ConfigEntryDisabler.USER, None], + None, + [ + { + "conversation_entity_id": "conversation.chatgpt_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + { + "conversation_entity_id": "conversation.chatgpt", + "device_disabled_by": DeviceEntryDisabler.USER, + "entity_disabled_by": RegistryEntryDisabler.DEVICE, + "device": 0, + }, + ], + 1, + ), + ( + [None, ConfigEntryDisabler.USER], + None, + [ + { + "conversation_entity_id": "conversation.chatgpt", + "device_disabled_by": DeviceEntryDisabler.USER, + "entity_disabled_by": RegistryEntryDisabler.DEVICE, + "device": 0, + }, + { + "conversation_entity_id": "conversation.chatgpt_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + ], + 0, + ), + ( + [ConfigEntryDisabler.USER, ConfigEntryDisabler.USER], + ConfigEntryDisabler.USER, + [ + { + "conversation_entity_id": "conversation.chatgpt", + "device_disabled_by": DeviceEntryDisabler.CONFIG_ENTRY, + "entity_disabled_by": RegistryEntryDisabler.CONFIG_ENTRY, + "device": 0, + }, + { + "conversation_entity_id": "conversation.chatgpt_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + ], + 0, + ), + ], +) +async def test_migration_from_v1_disabled( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry_disabled_by: list[ConfigEntryDisabler | None], + merged_config_entry_disabled_by: ConfigEntryDisabler | None, + conversation_subentry_data: list[dict[str, Any]], + main_config_entry: int, +) -> None: + """Test migration where the config entries are disabled.""" + # Create a v1 config entry with conversation options and an entity + options = { + "recommended": True, + "llm_hass_api": ["assist"], + "prompt": "You are a helpful assistant", + "chat_model": "gpt-4o-mini", + } + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_API_KEY: "1234"}, + options=options, + version=1, + title="ChatGPT", + disabled_by=config_entry_disabled_by[0], + ) + mock_config_entry.add_to_hass(hass) + mock_config_entry_2 = MockConfigEntry( + domain=DOMAIN, + data={CONF_API_KEY: "1234"}, + options=options, + version=1, + title="ChatGPT 2", + disabled_by=config_entry_disabled_by[1], + ) + mock_config_entry_2.add_to_hass(hass) + mock_config_entries = [mock_config_entry, mock_config_entry_2] + + device_1 = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, mock_config_entry.entry_id)}, + name=mock_config_entry.title, + manufacturer="OpenAI", + model="ChatGPT", + entry_type=dr.DeviceEntryType.SERVICE, + disabled_by=DeviceEntryDisabler.CONFIG_ENTRY, + ) + entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry.entry_id, + config_entry=mock_config_entry, + device_id=device_1.id, + suggested_object_id="chatgpt", + disabled_by=RegistryEntryDisabler.CONFIG_ENTRY, + ) + + device_2 = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_2.entry_id, + identifiers={(DOMAIN, mock_config_entry_2.entry_id)}, + name=mock_config_entry_2.title, + manufacturer="OpenAI", + model="ChatGPT", + entry_type=dr.DeviceEntryType.SERVICE, + ) + entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry_2.entry_id, + config_entry=mock_config_entry_2, + device_id=device_2.id, + suggested_object_id="chatgpt_2", + ) + + devices = [device_1, device_2] + + # Run migration + with patch( + "homeassistant.components.openai_conversation.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.disabled_by is merged_config_entry_disabled_by + assert entry.version == 2 + assert entry.minor_version == 4 + assert not entry.options + assert entry.title == "OpenAI Conversation" + assert len(entry.subentries) == 3 + conversation_subentries = [ + subentry + for subentry in entry.subentries.values() + if subentry.subentry_type == "conversation" + ] + assert len(conversation_subentries) == 2 + for subentry in conversation_subentries: + assert subentry.subentry_type == "conversation" + assert subentry.data == options + assert "ChatGPT" in subentry.title + ai_task_subentries = [ + subentry + for subentry in entry.subentries.values() + if subentry.subentry_type == "ai_task_data" + ] + assert len(ai_task_subentries) == 1 + assert ai_task_subentries[0].data == RECOMMENDED_AI_TASK_OPTIONS + assert ai_task_subentries[0].title == DEFAULT_AI_TASK_NAME + + assert not device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + assert not device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry_2.entry_id)} + ) + + for idx, subentry in enumerate(conversation_subentries): + subentry_data = conversation_subentry_data[idx] + entity = entity_registry.async_get(subentry_data["conversation_entity_id"]) + assert entity.unique_id == subentry.subentry_id + assert entity.config_subentry_id == subentry.subentry_id + assert entity.config_entry_id == entry.entry_id + assert entity.disabled_by is subentry_data["entity_disabled_by"] + + assert ( + device := device_registry.async_get_device( + identifiers={(DOMAIN, subentry.subentry_id)} + ) + ) + assert device.identifiers == {(DOMAIN, subentry.subentry_id)} + assert device.id == devices[subentry_data["device"]].id + assert device.config_entries == { + mock_config_entries[main_config_entry].entry_id + } + assert device.config_entries_subentries == { + mock_config_entries[main_config_entry].entry_id: {subentry.subentry_id} + } + assert device.disabled_by is subentry_data["device_disabled_by"] + + async def test_migration_from_v2_1( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -953,7 +1169,7 @@ async def test_migration_from_v2_1( assert len(entries) == 1 entry = entries[0] assert entry.version == 2 - assert entry.minor_version == 3 + assert entry.minor_version == 4 assert not entry.options assert entry.title == "ChatGPT" assert len(entry.subentries) == 3 # 2 conversation + 1 AI task @@ -1089,7 +1305,7 @@ async def test_migration_from_v2_2( assert len(entries) == 1 entry = entries[0] assert entry.version == 2 - assert entry.minor_version == 3 + assert entry.minor_version == 4 assert not entry.options assert entry.title == "ChatGPT" assert len(entry.subentries) == 2 @@ -1114,3 +1330,188 @@ async def test_migration_from_v2_2( ai_task_subentry = ai_task_subentries[0] assert ai_task_subentry.data == {"recommended": True} assert ai_task_subentry.title == "OpenAI AI Task" + + +@pytest.mark.parametrize( + ( + "config_entry_disabled_by", + "device_disabled_by", + "entity_disabled_by", + "setup_result", + "minor_version_after_migration", + "config_entry_disabled_by_after_migration", + "device_disabled_by_after_migration", + "entity_disabled_by_after_migration", + ), + [ + # Config entry not disabled, update device and entity disabled by config entry + ( + None, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + True, + 4, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + True, + 4, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + True, + 4, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + ), + ( + None, + None, + None, + True, + 4, + None, + None, + None, + ), + # Config entry disabled, migration does not run + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + False, + 3, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + ), + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + False, + 3, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + False, + 3, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + ), + ( + ConfigEntryDisabler.USER, + None, + None, + False, + 3, + ConfigEntryDisabler.USER, + None, + None, + ), + ], +) +async def test_migrate_entry_from_v2_3( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry_disabled_by: ConfigEntryDisabler | None, + device_disabled_by: DeviceEntryDisabler | None, + entity_disabled_by: RegistryEntryDisabler | None, + setup_result: bool, + minor_version_after_migration: int, + config_entry_disabled_by_after_migration: ConfigEntryDisabler | None, + device_disabled_by_after_migration: ConfigEntryDisabler | None, + entity_disabled_by_after_migration: RegistryEntryDisabler | None, +) -> None: + """Test migration from version 2.3.""" + # Create a v2.3 config entry with conversation subentries + conversation_subentry_id = "blabla" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_API_KEY: "test-api-key"}, + disabled_by=config_entry_disabled_by, + version=2, + minor_version=3, + subentries_data=[ + { + "data": RECOMMENDED_CONVERSATION_OPTIONS, + "subentry_id": conversation_subentry_id, + "subentry_type": "conversation", + "title": DEFAULT_CONVERSATION_NAME, + "unique_id": None, + }, + ], + ) + mock_config_entry.add_to_hass(hass) + + conversation_device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + config_subentry_id=conversation_subentry_id, + disabled_by=device_disabled_by, + identifiers={(DOMAIN, mock_config_entry.entry_id)}, + name=mock_config_entry.title, + manufacturer="OpenAI", + model="ChatGPT", + entry_type=dr.DeviceEntryType.SERVICE, + ) + conversation_entity = entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry.entry_id, + config_entry=mock_config_entry, + config_subentry_id=conversation_subentry_id, + disabled_by=entity_disabled_by, + device_id=conversation_device.id, + suggested_object_id="chatgpt", + ) + + # Verify initial state + assert mock_config_entry.version == 2 + assert mock_config_entry.minor_version == 3 + assert len(mock_config_entry.subentries) == 1 + assert mock_config_entry.disabled_by == config_entry_disabled_by + assert conversation_device.disabled_by == device_disabled_by + assert conversation_entity.disabled_by == entity_disabled_by + + # Run setup to trigger migration + with patch( + "homeassistant.components.openai_conversation.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert result is setup_result + await hass.async_block_till_done() + + # Verify migration completed + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + + # Check version and subversion were updated + assert entry.version == 2 + assert entry.minor_version == minor_version_after_migration + + # Check the disabled_by flag on config entry, device and entity are as expected + conversation_device = device_registry.async_get(conversation_device.id) + conversation_entity = entity_registry.async_get(conversation_entity.entity_id) + assert mock_config_entry.disabled_by == config_entry_disabled_by_after_migration + assert conversation_device.disabled_by == device_disabled_by_after_migration + assert conversation_entity.disabled_by == entity_disabled_by_after_migration From 855e8b08e92450b48c6ecde90f2bb74f1d8cd4e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Aug 2025 11:26:23 +0000 Subject: [PATCH 118/247] Bump version to 2025.8.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 b6f254e50eb71..349b8d9c9b815 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 5eadf909718eb..976892378d107 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0b3" +version = "2025.8.0b4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 94bade0202ed2d848d85a43c2a1843e8fb041887 Mon Sep 17 00:00:00 2001 From: David Poll Date: Wed, 6 Aug 2025 06:20:03 -0700 Subject: [PATCH 119/247] Fix zero-argument functions with as_function (#150062) --- homeassistant/helpers/template.py | 4 ++-- tests/helpers/test_template.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 85ee1e283093b..8e3106093aaf0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2030,7 +2030,7 @@ def apply(value, fn, *args, **kwargs): def as_function(macro: jinja2.runtime.Macro) -> Callable[..., Any]: """Turn a macro with a 'returns' keyword argument into a function that returns what that argument is called with.""" - def wrapper(value, *args, **kwargs): + def wrapper(*args, **kwargs): return_value = None def returns(value): @@ -2039,7 +2039,7 @@ def returns(value): return value # Call the callable with the value and other args - macro(value, *args, **kwargs, returns=returns) + macro(*args, **kwargs, returns=returns) return return_value # Remove "macro_" from the macro's name to avoid confusion in the wrapper's name diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 82b6434cf3fe3..85a2673f17d90 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -845,6 +845,23 @@ def test_as_function(hass: HomeAssistant) -> None: ) +def test_as_function_no_arguments(hass: HomeAssistant) -> None: + """Test as_function with no arguments.""" + assert ( + template.Template( + """ + {%- macro macro_get_hello(returns) -%} + {%- do returns("Hello") -%} + {%- endmacro -%} + {%- set get_hello = macro_get_hello | as_function -%} + {{ get_hello() }} + """, + hass, + ).async_render() + == "Hello" + ) + + def test_logarithm(hass: HomeAssistant) -> None: """Test logarithm.""" tests = [ From d18f6273a89e6d5df31a5001ece60b2dc02803b0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 6 Aug 2025 14:14:42 +0200 Subject: [PATCH 120/247] Fix update coordinator ContextVar log for custom integrations (#150100) --- homeassistant/helpers/update_coordinator.py | 2 +- tests/helpers/test_update_coordinator.py | 54 +++++++++++++++------ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 6b566797017d8..16f3b9b696421 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -92,7 +92,7 @@ def __init__( frame.report_usage( "relies on ContextVar, but should pass the config entry explicitly.", core_behavior=frame.ReportBehavior.ERROR, - custom_integration_behavior=frame.ReportBehavior.LOG, + custom_integration_behavior=frame.ReportBehavior.IGNORE, breaks_in_ha_version="2026.8", ) diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index b4216a3fc6d4a..57e80927e7ee6 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -942,17 +942,24 @@ async def test_config_entry_custom_integration( # Default without context should be None crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test") + assert crd.config_entry is None - assert ( - "Detected that integration 'my_integration' relies on ContextVar" - not in caplog.text - ) + # Should not log any warnings about ContextVar usage for custom integrations + frame_records = [ + record + for record in caplog.records + if record.name == "homeassistant.helpers.frame" + and record.levelno >= logging.WARNING + ] + assert len(frame_records) == 0 # Explicit None is OK caplog.clear() + crd = update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, name="test", config_entry=None ) + assert crd.config_entry is None assert ( "Detected that integration 'my_integration' relies on ContextVar" @@ -961,38 +968,53 @@ async def test_config_entry_custom_integration( # Explicit entry is OK caplog.clear() + crd = update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, name="test", config_entry=entry ) + assert crd.config_entry is entry - assert ( - "Detected that integration 'my_integration' relies on ContextVar" - not in caplog.text - ) + frame_records = [ + record + for record in caplog.records + if record.name == "homeassistant.helpers.frame" + and record.levelno >= logging.WARNING + ] + assert len(frame_records) == 0 # set ContextVar config_entries.current_entry.set(entry) # Default with ContextVar should match the ContextVar caplog.clear() + crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test") + assert crd.config_entry is entry - assert ( - "Detected that integration 'my_integration' relies on ContextVar" - not in caplog.text - ) + frame_records = [ + record + for record in caplog.records + if record.name == "homeassistant.helpers.frame" + and record.levelno >= logging.WARNING + ] + assert len(frame_records) == 0 # Explicit entry different from ContextVar not recommended, but should work another_entry = MockConfigEntry() caplog.clear() + crd = update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, name="test", config_entry=another_entry ) + assert crd.config_entry is another_entry - assert ( - "Detected that integration 'my_integration' relies on ContextVar" - not in caplog.text - ) + frame_records = [ + record + for record in caplog.records + if record.name == "homeassistant.helpers.frame" + and record.levelno >= logging.WARNING + ] + assert len(frame_records) == 0 async def test_listener_unsubscribe_releases_coordinator(hass: HomeAssistant) -> None: From 0478f43b4bf33cd8f0b8d26ccf5e7068a839118a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 6 Aug 2025 14:55:00 +0200 Subject: [PATCH 121/247] Bump holidays to 0.78 (#150103) --- 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 05cdd2738b623..dde50da1af3d7 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.77", "babel==2.15.0"] + "requirements": ["holidays==0.78", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 32edd5d3f6a0b..d230970272865 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.77"] + "requirements": ["holidays==0.78"] } diff --git a/requirements_all.txt b/requirements_all.txt index 10ebce6309a6e..62fb43312885d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1171,7 +1171,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.77 +holidays==0.78 # homeassistant.components.frontend home-assistant-frontend==20250805.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e888bad6847e7..45b5bd5e1e7ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1020,7 +1020,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.77 +holidays==0.78 # homeassistant.components.frontend home-assistant-frontend==20250805.0 From 2cf5badc17d1d6d5ee0d536a9aad0c6ec56f1712 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 6 Aug 2025 14:27:36 +0200 Subject: [PATCH 122/247] Enable disabled Ollama config entries after entry migration (#150105) --- homeassistant/components/ollama/__init__.py | 141 ++++-- .../components/ollama/config_flow.py | 2 +- tests/components/ollama/test_init.py | 412 +++++++++++++++++- 3 files changed, 513 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/ollama/__init__.py b/homeassistant/components/ollama/__init__.py index e16550c1e94a2..091e58dbe7f71 100644 --- a/homeassistant/components/ollama/__init__.py +++ b/homeassistant/components/ollama/__init__.py @@ -92,11 +92,15 @@ async def async_update_options(hass: HomeAssistant, entry: OllamaConfigEntry) -> async def async_migrate_integration(hass: HomeAssistant) -> None: """Migrate integration entry structure.""" - entries = hass.config_entries.async_entries(DOMAIN) + # Make sure we get enabled config entries first + entries = sorted( + hass.config_entries.async_entries(DOMAIN), + key=lambda e: e.disabled_by is not None, + ) if not any(entry.version == 1 for entry in entries): return - api_keys_entries: dict[str, ConfigEntry] = {} + url_entries: dict[str, tuple[ConfigEntry, bool]] = {} entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) @@ -112,33 +116,64 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: title=entry.title, unique_id=None, ) - if entry.data[CONF_URL] not in api_keys_entries: + if entry.data[CONF_URL] not in url_entries: use_existing = True - api_keys_entries[entry.data[CONF_URL]] = entry + all_disabled = all( + e.disabled_by is not None + for e in entries + if e.data[CONF_URL] == entry.data[CONF_URL] + ) + url_entries[entry.data[CONF_URL]] = (entry, all_disabled) - parent_entry = api_keys_entries[entry.data[CONF_URL]] + parent_entry, all_disabled = url_entries[entry.data[CONF_URL]] hass.config_entries.async_add_subentry(parent_entry, subentry) - conversation_entity = entity_registry.async_get_entity_id( + conversation_entity_id = entity_registry.async_get_entity_id( "conversation", DOMAIN, entry.entry_id, ) - if conversation_entity is not None: + device = device_registry.async_get_device( + identifiers={(DOMAIN, entry.entry_id)} + ) + + if conversation_entity_id is not None: + conversation_entity_entry = entity_registry.entities[conversation_entity_id] + entity_disabled_by = conversation_entity_entry.disabled_by + if ( + entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY + and not all_disabled + ): + # Device and entity registries don't update the disabled_by flag + # when moving a device or entity from one config entry to another, + # so we need to do it manually. + entity_disabled_by = ( + er.RegistryEntryDisabler.DEVICE + if device + else er.RegistryEntryDisabler.USER + ) entity_registry.async_update_entity( - conversation_entity, + conversation_entity_id, config_entry_id=parent_entry.entry_id, config_subentry_id=subentry.subentry_id, + disabled_by=entity_disabled_by, new_unique_id=subentry.subentry_id, ) - device = device_registry.async_get_device( - identifiers={(DOMAIN, entry.entry_id)} - ) if device is not None: + # Device and entity registries don't update the disabled_by flag when + # moving a device or entity from one config entry to another, so we + # need to do it manually. + device_disabled_by = device.disabled_by + if ( + device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY + and not all_disabled + ): + device_disabled_by = dr.DeviceEntryDisabler.USER device_registry.async_update_device( device.id, + disabled_by=device_disabled_by, new_identifiers={(DOMAIN, subentry.subentry_id)}, add_config_subentry_id=subentry.subentry_id, add_config_entry_id=parent_entry.entry_id, @@ -158,6 +193,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: if not use_existing: await hass.config_entries.async_remove(entry.entry_id) else: + _add_ai_task_subentry(hass, entry) hass.config_entries.async_update_entry( entry, title=DEFAULT_NAME, @@ -165,7 +201,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: data={CONF_URL: entry.data[CONF_URL]}, options={}, version=3, - minor_version=1, + minor_version=3, ) @@ -211,32 +247,69 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) -> ) if entry.version == 3 and entry.minor_version == 1: - # Add AI Task subentry with default options. We can only create a new - # subentry if we can find an existing model in the entry. The model - # was removed in the previous migration step, so we need to - # check the subentries for an existing model. - existing_model = next( - iter( - model - for subentry in entry.subentries.values() - if (model := subentry.data.get(CONF_MODEL)) is not None - ), - None, - ) - if existing_model: - hass.config_entries.async_add_subentry( - entry, - ConfigSubentry( - data=MappingProxyType({CONF_MODEL: existing_model}), - subentry_type="ai_task_data", - title=DEFAULT_AI_TASK_NAME, - unique_id=None, - ), - ) + _add_ai_task_subentry(hass, entry) hass.config_entries.async_update_entry(entry, minor_version=2) + if entry.version == 3 and entry.minor_version == 2: + # Fix migration where the disabled_by flag was not set correctly. + # We can currently only correct this for enabled config entries, + # because migration does not run for disabled config entries. This + # is asserted in tests, and if that behavior is changed, we should + # correct also disabled config entries. + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + entity_entries = er.async_entries_for_config_entry( + entity_registry, entry.entry_id + ) + if entry.disabled_by is None: + # If the config entry is not disabled, we need to set the disabled_by + # flag on devices to USER, and on entities to DEVICE, if they are set + # to CONFIG_ENTRY. + for device in devices: + if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY: + continue + device_registry.async_update_device( + device.id, + disabled_by=dr.DeviceEntryDisabler.USER, + ) + for entity in entity_entries: + if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY: + continue + entity_registry.async_update_entity( + entity.entity_id, + disabled_by=er.RegistryEntryDisabler.DEVICE, + ) + hass.config_entries.async_update_entry(entry, minor_version=3) + _LOGGER.debug( "Migration to version %s:%s successful", entry.version, entry.minor_version ) return True + + +def _add_ai_task_subentry(hass: HomeAssistant, entry: OllamaConfigEntry) -> None: + """Add AI Task subentry to the config entry.""" + # Add AI Task subentry with default options. We can only create a new + # subentry if we can find an existing model in the entry. The model + # was removed in the previous migration step, so we need to + # check the subentries for an existing model. + existing_model = next( + iter( + model + for subentry in entry.subentries.values() + if (model := subentry.data.get(CONF_MODEL)) is not None + ), + None, + ) + if existing_model: + hass.config_entries.async_add_subentry( + entry, + ConfigSubentry( + data=MappingProxyType({CONF_MODEL: existing_model}), + subentry_type="ai_task_data", + title=DEFAULT_AI_TASK_NAME, + unique_id=None, + ), + ) diff --git a/homeassistant/components/ollama/config_flow.py b/homeassistant/components/ollama/config_flow.py index cca917f6c2916..68deb00d20565 100644 --- a/homeassistant/components/ollama/config_flow.py +++ b/homeassistant/components/ollama/config_flow.py @@ -76,7 +76,7 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ollama.""" VERSION = 3 - MINOR_VERSION = 2 + MINOR_VERSION = 3 def __init__(self) -> None: """Initialize config flow.""" diff --git a/tests/components/ollama/test_init.py b/tests/components/ollama/test_init.py index 1db5730270410..766de8a7d6d79 100644 --- a/tests/components/ollama/test_init.py +++ b/tests/components/ollama/test_init.py @@ -1,5 +1,6 @@ """Tests for the Ollama integration.""" +from typing import Any from unittest.mock import patch from httpx import ConnectError @@ -7,9 +8,12 @@ from homeassistant.components import ollama from homeassistant.components.ollama.const import DOMAIN -from homeassistant.config_entries import ConfigSubentryData +from homeassistant.config_entries import ConfigEntryDisabler, ConfigSubentryData +from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er, llm +from homeassistant.helpers.device_registry import DeviceEntryDisabler +from homeassistant.helpers.entity_registry import RegistryEntryDisabler from homeassistant.setup import async_setup_component from . import TEST_OPTIONS @@ -96,7 +100,7 @@ async def test_migration_from_v1( await hass.async_block_till_done() assert mock_config_entry.version == 3 - assert mock_config_entry.minor_version == 2 + assert mock_config_entry.minor_version == 3 # After migration, parent entry should only have URL assert mock_config_entry.data == {ollama.CONF_URL: "http://localhost:11434"} assert mock_config_entry.options == {} @@ -223,7 +227,7 @@ async def test_migration_from_v1_with_multiple_urls( for idx, entry in enumerate(entries): assert entry.version == 3 - assert entry.minor_version == 2 + assert entry.minor_version == 3 assert not entry.options assert len(entry.subentries) == 2 @@ -332,7 +336,7 @@ async def test_migration_from_v1_with_same_urls( entry = entries[0] assert entry.version == 3 - assert entry.minor_version == 2 + assert entry.minor_version == 3 assert not entry.options # Two conversation subentries from the two original entries and 1 aitask subentry assert len(entry.subentries) == 3 @@ -365,6 +369,209 @@ async def test_migration_from_v1_with_same_urls( } +@pytest.mark.parametrize( + ( + "config_entry_disabled_by", + "merged_config_entry_disabled_by", + "conversation_subentry_data", + "main_config_entry", + ), + [ + ( + [ConfigEntryDisabler.USER, None], + None, + [ + { + "conversation_entity_id": "conversation.ollama_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + { + "conversation_entity_id": "conversation.ollama", + "device_disabled_by": DeviceEntryDisabler.USER, + "entity_disabled_by": RegistryEntryDisabler.DEVICE, + "device": 0, + }, + ], + 1, + ), + ( + [None, ConfigEntryDisabler.USER], + None, + [ + { + "conversation_entity_id": "conversation.ollama", + "device_disabled_by": DeviceEntryDisabler.USER, + "entity_disabled_by": RegistryEntryDisabler.DEVICE, + "device": 0, + }, + { + "conversation_entity_id": "conversation.ollama_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + ], + 0, + ), + ( + [ConfigEntryDisabler.USER, ConfigEntryDisabler.USER], + ConfigEntryDisabler.USER, + [ + { + "conversation_entity_id": "conversation.ollama", + "device_disabled_by": DeviceEntryDisabler.CONFIG_ENTRY, + "entity_disabled_by": RegistryEntryDisabler.CONFIG_ENTRY, + "device": 0, + }, + { + "conversation_entity_id": "conversation.ollama_2", + "device_disabled_by": None, + "entity_disabled_by": None, + "device": 1, + }, + ], + 0, + ), + ], +) +async def test_migration_from_v1_disabled( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry_disabled_by: list[ConfigEntryDisabler | None], + merged_config_entry_disabled_by: ConfigEntryDisabler | None, + conversation_subentry_data: list[dict[str, Any]], + main_config_entry: int, +) -> None: + """Test migration where the config entries are disabled.""" + # Create a v1 config entry with conversation options and an entity + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={"url": "http://localhost:11434", "model": "llama3.2:latest"}, + options=V1_TEST_OPTIONS, + version=1, + title="Ollama", + disabled_by=config_entry_disabled_by[0], + ) + mock_config_entry.add_to_hass(hass) + mock_config_entry_2 = MockConfigEntry( + domain=DOMAIN, + data={"url": "http://localhost:11434", "model": "llama3.2:latest"}, + options=V1_TEST_OPTIONS, + version=1, + title="Ollama 2", + disabled_by=config_entry_disabled_by[1], + ) + mock_config_entry_2.add_to_hass(hass) + mock_config_entries = [mock_config_entry, mock_config_entry_2] + + device_1 = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, mock_config_entry.entry_id)}, + name=mock_config_entry.title, + manufacturer="Ollama", + model="Ollama", + entry_type=dr.DeviceEntryType.SERVICE, + disabled_by=DeviceEntryDisabler.CONFIG_ENTRY, + ) + entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry.entry_id, + config_entry=mock_config_entry, + device_id=device_1.id, + suggested_object_id="ollama", + disabled_by=RegistryEntryDisabler.CONFIG_ENTRY, + ) + + device_2 = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_2.entry_id, + identifiers={(DOMAIN, mock_config_entry_2.entry_id)}, + name=mock_config_entry_2.title, + manufacturer="Ollama", + model="Ollama", + entry_type=dr.DeviceEntryType.SERVICE, + ) + entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry_2.entry_id, + config_entry=mock_config_entry_2, + device_id=device_2.id, + suggested_object_id="ollama_2", + ) + + devices = [device_1, device_2] + + # Run migration + with patch( + "homeassistant.components.ollama.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.disabled_by is merged_config_entry_disabled_by + assert entry.version == 3 + assert entry.minor_version == 3 + assert not entry.options + assert entry.title == "Ollama" + assert len(entry.subentries) == 3 + conversation_subentries = [ + subentry + for subentry in entry.subentries.values() + if subentry.subentry_type == "conversation" + ] + assert len(conversation_subentries) == 2 + for subentry in conversation_subentries: + assert subentry.subentry_type == "conversation" + assert subentry.data == {"model": "llama3.2:latest", **V1_TEST_OPTIONS} + assert "Ollama" in subentry.title + ai_task_subentries = [ + subentry + for subentry in entry.subentries.values() + if subentry.subentry_type == "ai_task_data" + ] + assert len(ai_task_subentries) == 1 + assert ai_task_subentries[0].data == {"model": "llama3.2:latest"} + assert ai_task_subentries[0].title == "Ollama AI Task" + + assert not device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + assert not device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry_2.entry_id)} + ) + + for idx, subentry in enumerate(conversation_subentries): + subentry_data = conversation_subentry_data[idx] + entity = entity_registry.async_get(subentry_data["conversation_entity_id"]) + assert entity.unique_id == subentry.subentry_id + assert entity.config_subentry_id == subentry.subentry_id + assert entity.config_entry_id == entry.entry_id + assert entity.disabled_by is subentry_data["entity_disabled_by"] + + assert ( + device := device_registry.async_get_device( + identifiers={(DOMAIN, subentry.subentry_id)} + ) + ) + assert device.identifiers == {(DOMAIN, subentry.subentry_id)} + assert device.id == devices[subentry_data["device"]].id + assert device.config_entries == { + mock_config_entries[main_config_entry].entry_id + } + assert device.config_entries_subentries == { + mock_config_entries[main_config_entry].entry_id: {subentry.subentry_id} + } + assert device.disabled_by is subentry_data["device_disabled_by"] + + async def test_migration_from_v2_1( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -457,7 +664,7 @@ async def test_migration_from_v2_1( assert len(entries) == 1 entry = entries[0] assert entry.version == 3 - assert entry.minor_version == 2 + assert entry.minor_version == 3 assert not entry.options assert entry.title == "Ollama" assert len(entry.subentries) == 3 @@ -546,7 +753,7 @@ async def test_migration_from_v2_2(hass: HomeAssistant) -> None: # Check migration to v3.1 assert mock_config_entry.version == 3 - assert mock_config_entry.minor_version == 2 + assert mock_config_entry.minor_version == 3 # Check that model was moved from main data to subentry assert mock_config_entry.data == {ollama.CONF_URL: "http://localhost:11434"} @@ -584,6 +791,197 @@ async def test_migration_from_v3_1_without_subentry(hass: HomeAssistant) -> None await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.version == 3 - assert mock_config_entry.minor_version == 2 + assert mock_config_entry.minor_version == 3 assert next(iter(mock_config_entry.subentries.values()), None) is None + + +@pytest.mark.parametrize( + ( + "config_entry_disabled_by", + "device_disabled_by", + "entity_disabled_by", + "setup_result", + "minor_version_after_migration", + "config_entry_disabled_by_after_migration", + "device_disabled_by_after_migration", + "entity_disabled_by_after_migration", + ), + [ + # Config entry not disabled, update device and entity disabled by config entry + ( + None, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + True, + 3, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + True, + 3, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + True, + 3, + None, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + ), + ( + None, + None, + None, + True, + 3, + None, + None, + None, + ), + # Config entry disabled, migration does not run + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + False, + 2, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.CONFIG_ENTRY, + RegistryEntryDisabler.CONFIG_ENTRY, + ), + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + False, + 2, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.DEVICE, + ), + ( + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + False, + 2, + ConfigEntryDisabler.USER, + DeviceEntryDisabler.USER, + RegistryEntryDisabler.USER, + ), + ( + ConfigEntryDisabler.USER, + None, + None, + False, + 2, + ConfigEntryDisabler.USER, + None, + None, + ), + ], +) +async def test_migrate_entry_from_v3_2( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry_disabled_by: ConfigEntryDisabler | None, + device_disabled_by: DeviceEntryDisabler | None, + entity_disabled_by: RegistryEntryDisabler | None, + setup_result: bool, + minor_version_after_migration: int, + config_entry_disabled_by_after_migration: ConfigEntryDisabler | None, + device_disabled_by_after_migration: ConfigEntryDisabler | None, + entity_disabled_by_after_migration: RegistryEntryDisabler | None, +) -> None: + """Test migration from version 3.2.""" + # Create a v3.2 config entry with conversation subentries + conversation_subentry_id = "blabla" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: "http://localhost:11434"}, + disabled_by=config_entry_disabled_by, + version=3, + minor_version=2, + subentries_data=[ + { + "data": V1_TEST_OPTIONS, + "subentry_id": conversation_subentry_id, + "subentry_type": "conversation", + "title": "Ollama", + "unique_id": None, + }, + { + "data": {"model": "llama3.2:latest"}, + "subentry_type": "ai_task_data", + "title": "Ollama AI Task", + "unique_id": None, + }, + ], + ) + mock_config_entry.add_to_hass(hass) + + conversation_device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + config_subentry_id=conversation_subentry_id, + disabled_by=device_disabled_by, + identifiers={(DOMAIN, mock_config_entry.entry_id)}, + name=mock_config_entry.title, + manufacturer="Ollama", + model="Ollama", + entry_type=dr.DeviceEntryType.SERVICE, + ) + conversation_entity = entity_registry.async_get_or_create( + "conversation", + DOMAIN, + mock_config_entry.entry_id, + config_entry=mock_config_entry, + config_subentry_id=conversation_subentry_id, + disabled_by=entity_disabled_by, + device_id=conversation_device.id, + suggested_object_id="ollama", + ) + + # Verify initial state + assert mock_config_entry.version == 3 + assert mock_config_entry.minor_version == 2 + assert len(mock_config_entry.subentries) == 2 + assert mock_config_entry.disabled_by == config_entry_disabled_by + assert conversation_device.disabled_by == device_disabled_by + assert conversation_entity.disabled_by == entity_disabled_by + + # Run setup to trigger migration + with patch( + "homeassistant.components.ollama.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert result is setup_result + await hass.async_block_till_done() + + # Verify migration completed + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + + # Check version and subversion were updated + assert entry.version == 3 + assert entry.minor_version == minor_version_after_migration + + # Check the disabled_by flag on config entry, device and entity are as expected + conversation_device = device_registry.async_get(conversation_device.id) + conversation_entity = entity_registry.async_get(conversation_entity.entity_id) + assert mock_config_entry.disabled_by == config_entry_disabled_by_after_migration + assert conversation_device.disabled_by == device_disabled_by_after_migration + assert conversation_entity.disabled_by == entity_disabled_by_after_migration From c4c14bee36d692c9efc5e4dfcfec44c46a7ac8cf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Aug 2025 15:22:46 +0200 Subject: [PATCH 123/247] Update frontend to 20250806.0 (#150106) Co-authored-by: Joost Lekkerkerker --- 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 7be7dd1def9eb..61ca88ba70ac0 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==20250805.0"] + "requirements": ["home-assistant-frontend==20250806.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 96707d39ccb9d..816b2e453e755 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==4.0.2 hass-nabucasa==0.111.1 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250805.0 +home-assistant-frontend==20250806.0 home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 62fb43312885d..bc44a8699660c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ hole==0.9.0 holidays==0.78 # homeassistant.components.frontend -home-assistant-frontend==20250805.0 +home-assistant-frontend==20250806.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45b5bd5e1e7ec..672615c939e3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ hole==0.9.0 holidays==0.78 # homeassistant.components.frontend -home-assistant-frontend==20250805.0 +home-assistant-frontend==20250806.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 From ad8ff7570d637900dc68302906c790d2e305310f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Aug 2025 13:34:19 +0000 Subject: [PATCH 124/247] Bump version to 2025.8.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 349b8d9c9b815..fc8f54b05bdaf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 976892378d107..5c87c8bcae516 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0b4" +version = "2025.8.0b5" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 6243517271eb73568af671283e3117bd519cd4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sun, 3 Aug 2025 11:19:08 +0200 Subject: [PATCH 125/247] Improve miele climate test coverage (#149859) --- .../components/miele/fixtures/5_devices.json | 124 +++++++++++ .../miele/fixtures/action_fridge_freezer.json | 31 +++ .../miele/fixtures/fridge_freezer.json | 9 +- .../miele/snapshots/test_climate.ambr | 208 +++++++++++++++++- tests/components/miele/test_climate.py | 31 ++- 5 files changed, 383 insertions(+), 20 deletions(-) create mode 100644 tests/components/miele/fixtures/action_fridge_freezer.json diff --git a/tests/components/miele/fixtures/5_devices.json b/tests/components/miele/fixtures/5_devices.json index 113babbd3f70a..2e76c1f6ef5f0 100644 --- a/tests/components/miele/fixtures/5_devices.json +++ b/tests/components/miele/fixtures/5_devices.json @@ -648,5 +648,129 @@ }, "batteryLevel": null } + }, + "DummyAppliance_12": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 12, + "value_localized": "Oven" + }, + "deviceName": "", + "protocolVersion": 4, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "16", + "techType": "H7660BP", + "matNumber": "11120960", + "swids": [] + }, + "xkmIdentLabel": { + "techType": "EK057", + "releaseVersion": "08.32" + } + }, + "state": { + "ProgramID": { + "value_raw": 356, + "value_localized": "Defrost", + "key_localized": "Program name" + }, + "status": { + "value_raw": 5, + "value_localized": "In use", + "key_localized": "status" + }, + "programType": { + "value_raw": 1, + "value_localized": "Program", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 3073, + "value_localized": "Heating-up phase", + "key_localized": "Program phase" + }, + "remainingTime": [0, 5], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": 2500, + "value_localized": 25.0, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": 1954, + "value_localized": 19.54, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": 2200, + "value_localized": 22.0, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": true + }, + "ambientLight": null, + "light": 1, + "elapsedTime": [0, 0], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } } } diff --git a/tests/components/miele/fixtures/action_fridge_freezer.json b/tests/components/miele/fixtures/action_fridge_freezer.json new file mode 100644 index 0000000000000..94ee43a90fe08 --- /dev/null +++ b/tests/components/miele/fixtures/action_fridge_freezer.json @@ -0,0 +1,31 @@ +{ + "processAction": [6], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [], + "targetTemperature": [ + { + "zone": 1, + "min": 1, + "max": 9 + }, + { + "zone": 2, + "min": -28, + "max": -14 + }, + { + "zone": 3, + "min": -30, + "max": -15 + } + ], + "deviceName": true, + "powerOn": false, + "powerOff": false, + "colors": [], + "modes": [1], + "runOnTime": [] +} diff --git a/tests/components/miele/fixtures/fridge_freezer.json b/tests/components/miele/fixtures/fridge_freezer.json index 5d091b9c74ee4..8ca28befc3580 100644 --- a/tests/components/miele/fixtures/fridge_freezer.json +++ b/tests/components/miele/fixtures/fridge_freezer.json @@ -53,6 +53,11 @@ "value_raw": -1800, "value_localized": -18.0, "unit": "Celsius" + }, + { + "value_raw": -2500, + "value_localized": -25.0, + "unit": "Celsius" } ], "coreTargetTemperature": [], @@ -68,8 +73,8 @@ "unit": "Celsius" }, { - "value_raw": -32768, - "value_localized": null, + "value_raw": -2800, + "value_localized": -28.0, "unit": "Celsius" } ], diff --git a/tests/components/miele/snapshots/test_climate.ambr b/tests/components/miele/snapshots/test_climate.ambr index 0fb24c893c42c..3b8b7488d9b37 100644 --- a/tests/components/miele/snapshots/test_climate.ambr +++ b/tests/components/miele/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_climate_states[platforms0-freezer][climate.freezer-entry] +# name: test_climate_states[freezer-platforms0][climate.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -41,7 +41,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.freezer-state] +# name: test_climate_states[freezer-platforms0][climate.freezer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -18, @@ -63,7 +63,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator-entry] +# name: test_climate_states[freezer-platforms0][climate.refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -105,7 +105,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator-state] +# name: test_climate_states[freezer-platforms0][climate.refrigerator-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 4, @@ -127,7 +127,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.freezer-entry] +# name: test_climate_states_api_push[freezer-platforms0][climate.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -169,7 +169,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.freezer-state] +# name: test_climate_states_api_push[freezer-platforms0][climate.freezer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -18, @@ -191,7 +191,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.refrigerator-entry] +# name: test_climate_states_api_push[freezer-platforms0][climate.refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -233,7 +233,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.refrigerator-state] +# name: test_climate_states_api_push[freezer-platforms0][climate.refrigerator-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 4, @@ -255,3 +255,195 @@ 'state': 'cool', }) # --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -14, + 'min_temp': -28, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_freezer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Freezer', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'freezer', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat2-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -18, + 'friendly_name': 'Fridge freezer Freezer', + 'hvac_modes': list([ + , + ]), + 'max_temp': -14, + 'min_temp': -28, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -18, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_freezer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_refrigerator', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Refrigerator', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'refrigerator', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 4, + 'friendly_name': 'Fridge freezer Refrigerator', + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': 4, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_refrigerator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -15, + 'min_temp': -30, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_zone_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Zone 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'zone_3', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat3-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -28, + 'friendly_name': 'Fridge freezer Zone 3', + 'hvac_modes': list([ + , + ]), + 'max_temp': -15, + 'min_temp': -30, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -25, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_zone_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index c4966430a9df2..392a67127075f 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -15,21 +15,13 @@ from tests.common import MockConfigEntry, snapshot_platform TEST_PLATFORM = CLIMATE_DOMAIN -pytestmark = [ - pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]), - pytest.mark.parametrize( - "load_action_file", - ["action_freezer.json"], - ids=[ - "freezer", - ], - ), -] +pytestmark = pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]) ENTITY_ID = "climate.freezer" SERVICE_SET_TEMPERATURE = "set_temperature" +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_climate_states( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -42,7 +34,24 @@ async def test_climate_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.parametrize("load_device_file", ["fridge_freezer.json"]) +@pytest.mark.parametrize( + "load_action_file", ["action_fridge_freezer.json"], ids=["fridge_freezer"] +) +async def test_climate_states_mulizone( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, +) -> None: + """Test climate entity state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_climate_states_api_push( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -56,6 +65,7 @@ async def test_climate_states_api_push( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_set_target( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -74,6 +84,7 @@ async def test_set_target( ) +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_api_failure( hass: HomeAssistant, mock_miele_client: MagicMock, From dd9bd50a7b825c838e30190a7263f38e4c5c4f11 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Wed, 6 Aug 2025 17:32:23 +0200 Subject: [PATCH 126/247] Deprecate Roborock battery feature (#150126) --- homeassistant/components/roborock/vacuum.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 058fffbdb1c3e..4bf3c49a72679 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -109,7 +109,6 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity): | VacuumEntityFeature.STOP | VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.FAN_SPEED - | VacuumEntityFeature.BATTERY | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.LOCATE | VacuumEntityFeature.CLEAN_SPOT @@ -142,11 +141,6 @@ def activity(self) -> VacuumActivity | None: assert self._device_status.state is not None return STATE_CODE_TO_STATE.get(self._device_status.state) - @property - def battery_level(self) -> int | None: - """Return the battery level of the vacuum cleaner.""" - return self._device_status.battery - @property def fan_speed(self) -> str | None: """Return the fan speed of the vacuum cleaner.""" From d791d66104e177943c568fe2898be2321c2f7dda Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Aug 2025 17:19:42 +0000 Subject: [PATCH 127/247] Bump version to 2025.8.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 fc8f54b05bdaf..c4033ac039c4e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 5c87c8bcae516..1b58380670345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0b5" +version = "2025.8.0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 319128043ec880972670baccbb3f8af87845f516 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 07:59:47 +0200 Subject: [PATCH 128/247] Make Tuya complex type handling explicit (#149677) --- homeassistant/components/tuya/models.py | 16 ++++++++++- homeassistant/components/tuya/sensor.py | 38 +++++++++++++++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/models.py b/homeassistant/components/tuya/models.py index b4afca83a8549..43e4c04c5186b 100644 --- a/homeassistant/components/tuya/models.py +++ b/homeassistant/components/tuya/models.py @@ -99,8 +99,22 @@ def from_json(cls, dpcode: DPCode, data: str) -> EnumTypeData | None: return cls(dpcode, **parsed) +class ComplexTypeData: + """Complex Type Data (for JSON/RAW parsing).""" + + @classmethod + def from_json(cls, data: str) -> Self: + """Load JSON string and return a ComplexTypeData object.""" + raise NotImplementedError("from_json is not implemented for this type") + + @classmethod + def from_raw(cls, data: str) -> Self: + """Decode base64 string and return a ComplexTypeData object.""" + raise NotImplementedError("from_raw is not implemented for this type") + + @dataclass -class ElectricityTypeData: +class ElectricityTypeData(ComplexTypeData): """Electricity Type Data.""" electriccurrent: str | None = None diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index ebb5c13f92a76..93b1780aeb9fe 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -40,13 +40,14 @@ UnitOfMeasurement, ) from .entity import TuyaEntity -from .models import ElectricityTypeData, EnumTypeData, IntegerTypeData +from .models import ComplexTypeData, ElectricityTypeData, EnumTypeData, IntegerTypeData @dataclass(frozen=True) class TuyaSensorEntityDescription(SensorEntityDescription): """Describes Tuya sensor entity.""" + complex_type: type[ComplexTypeData] | None = None subkey: str | None = None @@ -368,6 +369,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -376,6 +378,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -384,6 +387,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -392,6 +396,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -400,6 +405,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -408,6 +414,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -416,6 +423,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -424,6 +432,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -432,6 +441,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1254,6 +1264,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): translation_key="total_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1262,6 +1273,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1270,6 +1282,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1278,6 +1291,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1286,6 +1300,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1294,6 +1309,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1302,6 +1318,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1310,6 +1327,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1318,6 +1336,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1326,6 +1345,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), ), @@ -1424,7 +1444,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): _status_range: DeviceStatusRange | None = None _type: DPType | None = None - _type_data: IntegerTypeData | EnumTypeData | None = None + _type_data: IntegerTypeData | EnumTypeData | ComplexTypeData | None = None _uom: UnitOfMeasurement | None = None def __init__( @@ -1523,15 +1543,21 @@ def native_value(self) -> StateType: # Get subkey value from Json string. if self._type is DPType.JSON: - if self.entity_description.subkey is None: + if ( + self.entity_description.complex_type is None + or self.entity_description.subkey is None + ): return None - values = ElectricityTypeData.from_json(value) + values = self.entity_description.complex_type.from_json(value) return getattr(values, self.entity_description.subkey) if self._type is DPType.RAW: - if self.entity_description.subkey is None: + if ( + self.entity_description.complex_type is None + or self.entity_description.subkey is None + ): return None - values = ElectricityTypeData.from_raw(value) + values = self.entity_description.complex_type.from_raw(value) return getattr(values, self.entity_description.subkey) # Valid string or enum value From ee32992010a2b5abda7dc2b41e7fef375cbbe9c6 Mon Sep 17 00:00:00 2001 From: "Stefan H." <34062375+BlackBadPinguin@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:33:24 +0200 Subject: [PATCH 129/247] Fix Enigma2 startup hang (#149756) --- homeassistant/components/enigma2/coordinator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enigma2/coordinator.py b/homeassistant/components/enigma2/coordinator.py index 9710d7f547f2c..02e50c2cc0645 100644 --- a/homeassistant/components/enigma2/coordinator.py +++ b/homeassistant/components/enigma2/coordinator.py @@ -1,5 +1,6 @@ """Data update coordinator for the Enigma2 integration.""" +import asyncio import logging from openwebif.api import OpenWebIfDevice, OpenWebIfStatus @@ -30,6 +31,8 @@ LOGGER = logging.getLogger(__package__) +SETUP_TIMEOUT = 10 + type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator] @@ -79,7 +82,7 @@ def __init__(self, hass: HomeAssistant, config_entry: Enigma2ConfigEntry) -> Non async def _async_setup(self) -> None: """Provide needed data to the device info.""" - about = await self.device.get_about() + about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT) self.device.mac_address = about["info"]["ifaces"][0]["mac"] self.device_info["model"] = about["info"]["model"] self.device_info["manufacturer"] = about["info"]["brand"] From efcffd1016ab583581b460de090b156485058370 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:11:32 -0400 Subject: [PATCH 130/247] Fix dialog enhancement switch for Sonos Arc Ultra (#150116) --- homeassistant/components/sonos/const.py | 3 ++ homeassistant/components/sonos/speaker.py | 8 ++++ homeassistant/components/sonos/switch.py | 51 ++++++++++++++++++---- tests/components/sonos/conftest.py | 20 +++++++++ tests/components/sonos/test_switch.py | 52 ++++++++++++++++++++++- 5 files changed, 123 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 76e0a915060a5..440d9a3aea7ff 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -186,6 +186,9 @@ "ULTRA", ) MODELS_LINEIN_AND_TV = ("AMP",) +MODEL_SONOS_ARC_ULTRA = "SONOS ARC ULTRA" + +ATTR_SPEECH_ENHANCEMENT_ENABLED = "speech_enhance_enabled" AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1) AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5 diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index f5cfb84ec36fb..894d32fcb97e1 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -35,6 +35,7 @@ from .alarms import SonosAlarms from .const import ( + ATTR_SPEECH_ENHANCEMENT_ENABLED, AVAILABILITY_TIMEOUT, BATTERY_SCAN_INTERVAL, DOMAIN, @@ -157,6 +158,7 @@ def __init__( # Home theater self.audio_delay: int | None = None self.dialog_level: bool | None = None + self.speech_enhance_enabled: bool | None = None self.night_mode: bool | None = None self.sub_enabled: bool | None = None self.sub_crossover: int | None = None @@ -548,6 +550,11 @@ def async_dispatch_media_update(self, event: SonosEvent) -> None: @callback def async_update_volume(self, event: SonosEvent) -> None: """Update information about currently volume settings.""" + _LOGGER.debug( + "Updating volume for %s with event variables: %s", + self.zone_name, + event.variables, + ) self.event_stats.process(event) variables = event.variables @@ -565,6 +572,7 @@ def async_update_volume(self, event: SonosEvent) -> None: for bool_var in ( "dialog_level", + ATTR_SPEECH_ENHANCEMENT_ENABLED, "night_mode", "sub_enabled", "surround_enabled", diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 582845d10a212..653be229b22d9 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -19,7 +19,9 @@ from .alarms import SonosAlarms from .const import ( + ATTR_SPEECH_ENHANCEMENT_ENABLED, DOMAIN, + MODEL_SONOS_ARC_ULTRA, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM, SONOS_CREATE_SWITCHES, @@ -59,6 +61,7 @@ ATTR_SURROUND_ENABLED, ATTR_STATUS_LIGHT, ) +ALL_SUBST_FEATURES = (ATTR_SPEECH_ENHANCEMENT_ENABLED,) COORDINATOR_FEATURES = ATTR_CROSSFADE @@ -69,6 +72,14 @@ WEEKEND_DAYS = (0, 6) +# Mapping of model names to feature attributes that need to be substituted. +# This is used to handle differences in attributes across Sonos models. +MODEL_FEATURE_SUBSTITUTIONS: dict[str, dict[str, str]] = { + MODEL_SONOS_ARC_ULTRA: { + ATTR_SPEECH_ENHANCEMENT: ATTR_SPEECH_ENHANCEMENT_ENABLED, + }, +} + async def async_setup_entry( hass: HomeAssistant, @@ -92,6 +103,13 @@ async def _async_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> N def available_soco_attributes(speaker: SonosSpeaker) -> list[str]: features = [] + for feature_type in ALL_SUBST_FEATURES: + try: + if (state := getattr(speaker.soco, feature_type, None)) is not None: + setattr(speaker, feature_type, state) + except SoCoSlaveException: + pass + for feature_type in ALL_FEATURES: try: if (state := getattr(speaker.soco, feature_type, None)) is not None: @@ -107,12 +125,23 @@ async def _async_create_switches(speaker: SonosSpeaker) -> None: available_soco_attributes, speaker ) for feature_type in available_features: + attribute_key = MODEL_FEATURE_SUBSTITUTIONS.get( + speaker.model_name.upper(), {} + ).get(feature_type, feature_type) _LOGGER.debug( - "Creating %s switch on %s", + "Creating %s switch on %s attribute %s", feature_type, speaker.zone_name, + attribute_key, + ) + entities.append( + SonosSwitchEntity( + feature_type=feature_type, + attribute_key=attribute_key, + speaker=speaker, + config_entry=config_entry, + ) ) - entities.append(SonosSwitchEntity(feature_type, speaker, config_entry)) async_add_entities(entities) config_entry.async_on_unload( @@ -127,11 +156,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): """Representation of a Sonos feature switch.""" def __init__( - self, feature_type: str, speaker: SonosSpeaker, config_entry: SonosConfigEntry + self, + feature_type: str, + attribute_key: str, + speaker: SonosSpeaker, + config_entry: SonosConfigEntry, ) -> None: """Initialize the switch.""" super().__init__(speaker, config_entry) - self.feature_type = feature_type + self.attribute_key = attribute_key self.needs_coordinator = feature_type in COORDINATOR_FEATURES self._attr_entity_category = EntityCategory.CONFIG self._attr_translation_key = feature_type @@ -149,15 +182,15 @@ async def _async_fallback_poll(self) -> None: @soco_error() def poll_state(self) -> None: """Poll the current state of the switch.""" - state = getattr(self.soco, self.feature_type) - setattr(self.speaker, self.feature_type, state) + state = getattr(self.soco, self.attribute_key) + setattr(self.speaker, self.attribute_key, state) @property def is_on(self) -> bool: """Return True if entity is on.""" if self.needs_coordinator and not self.speaker.is_coordinator: - return cast(bool, getattr(self.speaker.coordinator, self.feature_type)) - return cast(bool, getattr(self.speaker, self.feature_type)) + return cast(bool, getattr(self.speaker.coordinator, self.attribute_key)) + return cast(bool, getattr(self.speaker, self.attribute_key)) def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" @@ -175,7 +208,7 @@ def send_command(self, enable: bool) -> None: else: soco = self.soco try: - setattr(soco, self.feature_type, enable) + setattr(soco, self.attribute_key, enable) except SoCoUPnPException as exc: _LOGGER.warning("Could not toggle %s: %s", self.entity_id, exc) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index d3de2a889d51c..0cdc17c55a629 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -882,3 +882,23 @@ def ungroup_speakers(coordinator: MockSoCo, group_member: MockSoCo) -> None: ) coordinator.zoneGroupTopology.subscribe.return_value._callback(event) group_member.zoneGroupTopology.subscribe.return_value._callback(event) + + +def create_rendering_control_event( + soco: MockSoCo, +) -> SonosMockEvent: + """Create a Sonos Event for speaker rendering control.""" + variables = { + "dialog_level": 1, + "speech_enhance_enable": 1, + "surround_level": 6, + "music_surround_level": 4, + "audio_delay": 0, + "audio_delay_left_rear": 0, + "audio_delay_right_rear": 0, + "night_mode": 0, + "surround_enabled": 1, + "surround_mode": 1, + "height_channel_level": 1, + } + return SonosMockEvent(soco, soco.renderingControl, variables) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 04457ee95c773..c7df2062b0f31 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -6,13 +6,18 @@ import pytest -from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER +from homeassistant.components.sonos.const import ( + DATA_SONOS_DISCOVERY_MANAGER, + MODEL_SONOS_ARC_ULTRA, +) from homeassistant.components.sonos.switch import ( ATTR_DURATION, ATTR_ID, ATTR_INCLUDE_LINKED_ZONES, ATTR_PLAY_MODE, ATTR_RECURRENCE, + ATTR_SPEECH_ENHANCEMENT, + ATTR_SPEECH_ENHANCEMENT_ENABLED, ATTR_VOLUME, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -29,7 +34,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from .conftest import MockSoCo, SonosMockEvent +from .conftest import MockSoCo, SonosMockEvent, create_rendering_control_event from tests.common import async_fire_time_changed @@ -142,6 +147,49 @@ async def test_switch_attributes( assert touch_controls_state.state == STATE_ON +@pytest.mark.parametrize( + ("model", "attribute"), + [ + ("Sonos One SL", ATTR_SPEECH_ENHANCEMENT), + (MODEL_SONOS_ARC_ULTRA.lower(), ATTR_SPEECH_ENHANCEMENT_ENABLED), + ], +) +async def test_switch_speech_enhancement( + hass: HomeAssistant, + async_setup_sonos, + soco: MockSoCo, + speaker_info: dict[str, str], + entity_registry: er.EntityRegistry, + model: str, + attribute: str, +) -> None: + """Tests the speech enhancement switch and attribute substitution for different models.""" + entity_id = "switch.zone_a_speech_enhancement" + speaker_info["model_name"] = model + soco.get_speaker_info.return_value = speaker_info + setattr(soco, attribute, True) + await async_setup_sonos() + switch = entity_registry.entities[entity_id] + state = hass.states.get(switch.entity_id) + assert state.state == STATE_ON + + event = create_rendering_control_event(soco) + event.variables[attribute] = False + soco.renderingControl.subscribe.return_value._callback(event) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(switch.entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert getattr(soco, attribute) is True + + @pytest.mark.parametrize( ("service", "expected_result"), [ From 8edc5f03591027216543e4927d189cfe58e21dd5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 6 Aug 2025 13:56:44 -0400 Subject: [PATCH 131/247] Bump ZHA to 0.0.67 (#150132) --- 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 38ce08aa782d4..9842fa7a0f352 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.66"], + "requirements": ["zha==0.0.67"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index bc44a8699660c..74f100168a630 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3203,7 +3203,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.66 +zha==0.0.67 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 672615c939e3f..bb48833de9a42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2647,7 +2647,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.66 +zha==0.0.67 # homeassistant.components.zwave_js zwave-js-server-python==0.67.1 From 6f4d405b269e71983c37fb8cd678cd1a885b14ef Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 8 Aug 2025 19:33:16 +0200 Subject: [PATCH 132/247] Bump airOS to 0.2.6 improving device class matching more devices (#150134) --- homeassistant/components/airos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airos/conftest.py | 2 +- ..._ap-ptp.json => airos_loco5ac_ap-ptp.json} | 473 ++++++++++-------- .../airos/snapshots/test_diagnostics.ambr | 10 + 6 files changed, 274 insertions(+), 217 deletions(-) rename tests/components/airos/fixtures/{airos_ap-ptp.json => airos_loco5ac_ap-ptp.json} (80%) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index 758902bbaa226..b9bd2db1ae478 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.2.4"] + "requirements": ["airos==0.2.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 74f100168a630..f8c452ec06f77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.4 +airos==0.2.6 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb48833de9a42..0acf09d2df9af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.4 +airos==0.2.6 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/tests/components/airos/conftest.py b/tests/components/airos/conftest.py index b17908e801aad..5443f79a97630 100644 --- a/tests/components/airos/conftest.py +++ b/tests/components/airos/conftest.py @@ -15,7 +15,7 @@ @pytest.fixture def ap_fixture(): """Load fixture data for AP mode.""" - json_data = load_json_object_fixture("airos_ap-ptp.json", DOMAIN) + json_data = load_json_object_fixture("airos_loco5ac_ap-ptp.json", DOMAIN) return AirOSData.from_dict(json_data) diff --git a/tests/components/airos/fixtures/airos_ap-ptp.json b/tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json similarity index 80% rename from tests/components/airos/fixtures/airos_ap-ptp.json rename to tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json index 06d13ba11015b..a033a82411c89 100644 --- a/tests/components/airos/fixtures/airos_ap-ptp.json +++ b/tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json @@ -1,132 +1,194 @@ { "chain_names": [ - { "number": 1, "name": "Chain 0" }, - { "number": 2, "name": "Chain 1" } + { + "name": "Chain 0", + "number": 1 + }, + { + "name": "Chain 1", + "number": 2 + } ], + "derived": { + "access_point": true, + "mac": "01:23:45:67:89:AB", + "mac_interface": "br0", + "ptmp": false, + "ptp": true, + "station": false + }, + "firewall": { + "eb6tables": false, + "ebtables": false, + "ip6tables": false, + "iptables": false + }, + "genuine": "/images/genuine.png", + "gps": { + "fix": 0, + "lat": 52.379894, + "lon": 4.901608 + }, "host": { - "hostname": "NanoStation 5AC ap name", + "cpuload": 10.10101, "device_id": "03aa0d0b40fed0a47088293584ef5432", - "uptime": 264888, + "devmodel": "NanoStation 5AC loco", + "freeram": 16564224, + "fwversion": "v8.7.17", + "height": 3, + "hostname": "NanoStation 5AC ap name", + "loadavg": 0.412598, + "netrole": "bridge", "power_time": 268683, + "temperature": 0, "time": "2025-06-23 23:06:42", "timestamp": 2668313184, - "fwversion": "v8.7.17", - "devmodel": "NanoStation 5AC loco", - "netrole": "bridge", - "loadavg": 0.412598, "totalram": 63447040, - "freeram": 16564224, - "temperature": 0, - "cpuload": 10.10101, - "height": 3 + "uptime": 264888 }, - "genuine": "/images/genuine.png", + "interfaces": [ + { + "enabled": true, + "hwaddr": "01:23:45:67:89:AB", + "ifname": "eth0", + "mtu": 1500, + "status": { + "cable_len": 18, + "duplex": true, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": true, + "rx_bytes": 3984971949, + "rx_dropped": 0, + "rx_errors": 4, + "rx_packets": 73564835, + "snr": [30, 30, 30, 30], + "speed": 1000, + "tx_bytes": 209900085624, + "tx_dropped": 10, + "tx_errors": 0, + "tx_packets": 185866883 + } + }, + { + "enabled": true, + "hwaddr": "01:23:45:67:89:AB", + "ifname": "ath0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": false, + "rx_bytes": 206938324766, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 149767200, + "snr": null, + "speed": 0, + "tx_bytes": 5265602738, + "tx_dropped": 2005, + "tx_errors": 0, + "tx_packets": 52980390 + } + }, + { + "enabled": true, + "hwaddr": "01:23:45:67:89:AB", + "ifname": "br0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": [ + { + "addr": "fe80::eea:14ff:fea4:89cd", + "plen": 64 + } + ], + "ipaddr": "192.168.1.2", + "plugged": true, + "rx_bytes": 204802727, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 1791592, + "snr": null, + "speed": 0, + "tx_bytes": 236295176, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 298119 + } + } + ], + "ntpclient": {}, + "portfw": false, + "provmode": {}, "services": { + "airview": 2, + "dhcp6d_stateful": false, "dhcpc": false, "dhcpd": false, - "dhcp6d_stateful": false, - "pppoe": false, - "airview": 2 + "pppoe": false }, - "firewall": { - "iptables": false, - "ebtables": false, - "ip6tables": false, - "eb6tables": false + "unms": { + "status": 0, + "timestamp": null }, - "portfw": false, "wireless": { - "essid": "DemoSSID", - "mode": "ap-ptp", - "ieeemode": "11ACVHT80", - "band": 2, - "compat_11n": 0, - "hide_essid": 0, - "apmac": "01:23:45:67:89:AB", "antenna_gain": 13, - "frequency": 5500, + "apmac": "01:23:45:67:89:AB", + "aprepeater": false, + "band": 2, + "cac_state": 0, + "cac_timeout": 0, "center1_freq": 5530, + "chanbw": 80, + "compat_11n": 0, + "count": 1, "dfs": 1, "distance": 0, - "security": "WPA2", + "essid": "DemoSSID", + "frequency": 5500, + "hide_essid": 0, + "ieeemode": "11ACVHT80", + "mode": "ap-ptp", "noisef": -89, - "txpower": -3, - "aprepeater": false, - "rstatus": 5, - "chanbw": 80, - "rx_chainmask": 3, - "tx_chainmask": 3, "nol_state": 0, "nol_timeout": 0, - "cac_state": 0, - "cac_timeout": 0, - "rx_idx": 8, - "rx_nss": 2, - "tx_idx": 9, - "tx_nss": 2, - "throughput": { "tx": 222, "rx": 9907 }, - "service": { "time": 267181, "link": 266003 }, "polling": { + "atpc_status": 2, "cb_capacity": 593970, "dl_capacity": 647400, - "ul_capacity": 540540, - "use": 48, - "tx_use": 6, - "rx_use": 42, - "atpc_status": 2, + "ff_cap_rep": false, "fixed_frame": false, + "flex_mode": null, "gps_sync": false, - "ff_cap_rep": false + "rx_use": 42, + "tx_use": 6, + "ul_capacity": 540540, + "use": 48 + }, + "rstatus": 5, + "rx_chainmask": 3, + "rx_idx": 8, + "rx_nss": 2, + "security": "WPA2", + "service": { + "link": 266003, + "time": 267181 }, - "count": 1, "sta": [ { - "mac": "01:23:45:67:89:AB", - "lastip": "192.168.1.2", - "signal": -59, - "rssi": 37, - "noisefloor": -89, - "chainrssi": [35, 32, 0], - "tx_idx": 9, - "rx_idx": 8, - "tx_nss": 2, - "rx_nss": 2, - "tx_latency": 0, - "distance": 1, - "tx_packets": 0, - "tx_lretries": 0, - "tx_sretries": 0, - "uptime": 170281, - "dl_signal_expect": -80, - "ul_signal_expect": -55, - "cb_capacity_expect": 416000, - "dl_capacity_expect": 208000, - "ul_capacity_expect": 624000, - "dl_rate_expect": 3, - "ul_rate_expect": 8, - "dl_linkscore": 100, - "ul_linkscore": 86, - "dl_avg_linkscore": 100, - "ul_avg_linkscore": 88, - "tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430], - "stats": { - "rx_bytes": 206938324814, - "rx_packets": 149767200, - "rx_pps": 846, - "tx_bytes": 5265602739, - "tx_packets": 52980390, - "tx_pps": 0 - }, "airmax": { "actual_priority": 0, + "atpc_status": 2, "beam": 0, - "desired_priority": 0, "cb_capacity": 593970, + "desired_priority": 0, "dl_capacity": 647400, - "ul_capacity": 540540, - "atpc_status": 2, "rx": { - "usage": 42, "cinr": 31, "evm": [ [ @@ -141,10 +203,10 @@ 34, 33, 34, 34, 34, 34, 34, 35, 35, 35, 34, 35, 33, 34, 34, 34, 34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35 ] - ] + ], + "usage": 42 }, "tx": { - "usage": 6, "cinr": 31, "evm": [ [ @@ -159,142 +221,127 @@ 38, 37, 37, 37, 38, 37, 38, 37, 37, 37, 37, 37, 36, 37, 37, 37, 37, 37, 37, 38, 37, 37, 38, 37, 36, 37, 37, 37, 37, 37, 37, 37 ] - ] - } + ], + "usage": 6 + }, + "ul_capacity": 540540 }, + "airos_connected": true, + "cb_capacity_expect": 416000, + "chainrssi": [35, 32, 0], + "distance": 1, + "dl_avg_linkscore": 100, + "dl_capacity_expect": 208000, + "dl_linkscore": 100, + "dl_rate_expect": 3, + "dl_signal_expect": -80, "last_disc": 1, + "lastip": "192.168.1.2", + "mac": "01:23:45:67:89:AB", + "noisefloor": -89, "remote": { "age": 1, - "device_id": "d4f4cdf82961e619328a8f72f8d7653b", - "hostname": "NanoStation 5AC sta name", - "platform": "NanoStation 5AC loco", - "version": "WA.ar934x.v8.7.17.48152.250620.2132", - "time": "2025-06-23 23:13:54", - "cpuload": 43.564301, - "temperature": 0, - "totalram": 63447040, - "freeram": 14290944, - "netrole": "bridge", - "mode": "sta-ptp", - "sys_id": "0xe7fa", - "tx_throughput": 16023, - "rx_throughput": 251, - "uptime": 265320, - "power_time": 268512, - "compat_11n": 0, - "signal": -58, - "rssi": 38, - "noisefloor": -90, - "tx_power": -4, - "distance": 1, - "rx_chainmask": 3, - "chainrssi": [33, 37, 0], - "tx_ratedata": [ - 14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154 - ], - "tx_bytes": 212308148210, - "rx_bytes": 3624206478, + "airview": 2, "antenna_gain": 13, "cable_loss": 0, - "height": 2, + "chainrssi": [33, 37, 0], + "compat_11n": 0, + "cpuload": 43.564301, + "device_id": "d4f4cdf82961e619328a8f72f8d7653b", + "distance": 1, "ethlist": [ { - "ifname": "eth0", + "cable_len": 14, + "duplex": true, "enabled": true, + "ifname": "eth0", "plugged": true, - "duplex": true, - "speed": 1000, "snr": [30, 30, 29, 30], - "cable_len": 14 + "speed": 1000 } ], - "ipaddr": ["192.168.1.2"], + "freeram": 14290944, + "gps": { + "alt": null, + "dim": null, + "dop": null, + "fix": 0, + "lat": 52.379894, + "lon": 4.901608, + "sats": null, + "time_synced": null + }, + "height": 2, + "hostname": "NanoStation 5AC sta name", "ip6addr": ["fe80::eea:14ff:fea4:89ab"], - "gps": { "lat": "52.379894", "lon": "4.901608", "fix": 0 }, + "ipaddr": ["192.168.1.2"], + "mode": "sta-ptp", + "netrole": "bridge", + "noisefloor": -90, "oob": false, - "unms": { "status": 0, "timestamp": null }, - "airview": 2, - "service": { "time": 267195, "link": 265996 } + "platform": "NanoStation 5AC loco", + "power_time": 268512, + "rssi": 38, + "rx_bytes": 3624206478, + "rx_chainmask": 3, + "rx_throughput": 251, + "service": { + "link": 265996, + "time": 267195 + }, + "signal": -58, + "sys_id": "0xe7fa", + "temperature": 0, + "time": "2025-06-23 23:13:54", + "totalram": 63447040, + "tx_bytes": 212308148210, + "tx_power": -4, + "tx_ratedata": [ + 14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154 + ], + "tx_throughput": 16023, + "unms": { + "status": 0, + "timestamp": null + }, + "uptime": 265320, + "version": "WA.ar934x.v8.7.17.48152.250620.2132" }, - "airos_connected": true + "rssi": 37, + "rx_idx": 8, + "rx_nss": 2, + "signal": -59, + "stats": { + "rx_bytes": 206938324814, + "rx_packets": 149767200, + "rx_pps": 846, + "tx_bytes": 5265602739, + "tx_packets": 52980390, + "tx_pps": 0 + }, + "tx_idx": 9, + "tx_latency": 0, + "tx_lretries": 0, + "tx_nss": 2, + "tx_packets": 0, + "tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430], + "tx_sretries": 0, + "ul_avg_linkscore": 88, + "ul_capacity_expect": 624000, + "ul_linkscore": 86, + "ul_rate_expect": 8, + "ul_signal_expect": -55, + "uptime": 170281 } ], - "sta_disconnected": [] - }, - "interfaces": [ - { - "ifname": "eth0", - "hwaddr": "01:23:45:67:89:AB", - "enabled": true, - "mtu": 1500, - "status": { - "plugged": true, - "tx_bytes": 209900085624, - "rx_bytes": 3984971949, - "tx_packets": 185866883, - "rx_packets": 73564835, - "tx_errors": 0, - "rx_errors": 4, - "tx_dropped": 10, - "rx_dropped": 0, - "ipaddr": "0.0.0.0", - "speed": 1000, - "duplex": true, - "snr": [30, 30, 30, 30], - "cable_len": 18, - "ip6addr": null - } + "sta_disconnected": [], + "throughput": { + "rx": 9907, + "tx": 222 }, - { - "ifname": "ath0", - "hwaddr": "01:23:45:67:89:AB", - "enabled": true, - "mtu": 1500, - "status": { - "plugged": false, - "tx_bytes": 5265602738, - "rx_bytes": 206938324766, - "tx_packets": 52980390, - "rx_packets": 149767200, - "tx_errors": 0, - "rx_errors": 0, - "tx_dropped": 2005, - "rx_dropped": 0, - "ipaddr": "0.0.0.0", - "speed": 0, - "duplex": false, - "snr": null, - "cable_len": null, - "ip6addr": null - } - }, - { - "ifname": "br0", - "hwaddr": "01:23:45:67:89:AB", - "enabled": true, - "mtu": 1500, - "status": { - "plugged": true, - "tx_bytes": 236295176, - "rx_bytes": 204802727, - "tx_packets": 298119, - "rx_packets": 1791592, - "tx_errors": 0, - "rx_errors": 0, - "tx_dropped": 0, - "rx_dropped": 0, - "ipaddr": "192.168.1.2", - "speed": 0, - "duplex": false, - "snr": null, - "cable_len": null, - "ip6addr": [{ "addr": "fe80::eea:14ff:fea4:89cd", "plen": 64 }] - } - } - ], - "provmode": {}, - "ntpclient": {}, - "unms": { "status": 0, "timestamp": null }, - "gps": { "lat": 52.379894, "lon": 4.901608, "fix": 0 }, - "derived": { "mac": "01:23:45:67:89:AB", "mac_interface": "br0" } + "tx_chainmask": 3, + "tx_idx": 9, + "tx_nss": 2, + "txpower": -3 + } } diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr index bc2dedc905aa6..574dbf689497c 100644 --- a/tests/components/airos/snapshots/test_diagnostics.ambr +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -13,8 +13,12 @@ }), ]), 'derived': dict({ + 'access_point': True, 'mac': '**REDACTED**', 'mac_interface': 'br0', + 'ptmp': False, + 'ptp': True, + 'station': False, }), 'firewall': dict({ 'eb6tables': False, @@ -164,6 +168,7 @@ 'dl_capacity': 647400, 'ff_cap_rep': False, 'fixed_frame': False, + 'flex_mode': None, 'gps_sync': False, 'rx_use': 42, 'tx_use': 6, @@ -515,9 +520,14 @@ ]), 'freeram': 14290944, 'gps': dict({ + 'alt': None, + 'dim': None, + 'dop': None, 'fix': 0, 'lat': '**REDACTED**', 'lon': '**REDACTED**', + 'sats': None, + 'time_synced': None, }), 'height': 2, 'hostname': '**REDACTED**', From 42a3bef34aebf5d8d9fb54f1277de7bff9c1ae47 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 8 Aug 2025 22:12:18 +0200 Subject: [PATCH 133/247] Handle HusqvarnaWSClientError (#150145) --- homeassistant/components/husqvarna_automower/coordinator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 91adc8c75ecfc..262f923e99cdd 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -12,6 +12,7 @@ ApiError, AuthError, HusqvarnaTimeoutError, + HusqvarnaWSClientError, HusqvarnaWSServerHandshakeError, ) from aioautomower.model import MowerDictionary @@ -142,7 +143,7 @@ async def client_listen( # Reset reconnect time after successful connection self.reconnect_time = DEFAULT_RECONNECT_TIME await automower_client.start_listening() - except HusqvarnaWSServerHandshakeError as err: + except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err: _LOGGER.debug( "Failed to connect to websocket. Trying to reconnect: %s", err, From 2223bdb48e8d51d72e25f1c77e27ca6b64bfa4d0 Mon Sep 17 00:00:00 2001 From: Marco Gasparini Date: Fri, 8 Aug 2025 22:29:50 +0200 Subject: [PATCH 134/247] Fix Progettihwsw config flow (#150149) --- homeassistant/components/progettihwsw/config_flow.py | 6 +++--- tests/components/progettihwsw/test_config_flow.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index 8818eff2d8125..826d5872d7cda 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -30,9 +30,9 @@ async def validate_input(hass: HomeAssistant, data): return { "title": is_valid["title"], - "relay_count": is_valid["relay_count"], - "input_count": is_valid["input_count"], - "is_old": is_valid["is_old"], + "relay_count": is_valid["relays"], + "input_count": is_valid["inputs"], + "is_old": is_valid["temps"], } diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 8dcc69173467c..c41c88ec95087 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -12,9 +12,9 @@ mock_value_step_user = { "title": "1R & 1IN Board", - "relay_count": 1, - "input_count": 1, - "is_old": False, + "relays": 1, + "inputs": 1, + "temps": False, } From c653bfff9f247ee4d47962e80f11a79e74ae2e5d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 7 Aug 2025 13:31:55 +0200 Subject: [PATCH 135/247] Bump imgw_pib to version 1.5.3 (#150178) --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index e65ccf35fb58f..145690487d7c0 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["imgw_pib==1.5.2"] + "requirements": ["imgw_pib==1.5.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index f8c452ec06f77..e312782fbbf4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1240,7 +1240,7 @@ ihcsdk==2.8.5 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.2 +imgw_pib==1.5.3 # homeassistant.components.incomfort incomfort-client==0.6.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0acf09d2df9af..d09c83faf81ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1074,7 +1074,7 @@ igloohome-api==0.1.1 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.2 +imgw_pib==1.5.3 # homeassistant.components.incomfort incomfort-client==0.6.9 From 7951e822be81c4ef939ee78117275b4a9c8e78cd Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 7 Aug 2025 15:22:36 +0200 Subject: [PATCH 136/247] Fix description of `button.press` action (#150181) --- homeassistant/components/button/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/button/strings.json b/homeassistant/components/button/strings.json index f552e9ae12b57..49a70ba9ffab8 100644 --- a/homeassistant/components/button/strings.json +++ b/homeassistant/components/button/strings.json @@ -25,7 +25,7 @@ "services": { "press": { "name": "Press", - "description": "Press the button entity." + "description": "Presses a button entity." } } } From 4765d9da923c568732e53a90da88c5a9713ea9db Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:38:27 +0200 Subject: [PATCH 137/247] Migrate unique_id only if monitor_id is present in Uptime Kuma (#150197) --- homeassistant/components/uptime_kuma/coordinator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/uptime_kuma/coordinator.py b/homeassistant/components/uptime_kuma/coordinator.py index 58eed420fd828..df64b12f8e9bb 100644 --- a/homeassistant/components/uptime_kuma/coordinator.py +++ b/homeassistant/components/uptime_kuma/coordinator.py @@ -104,7 +104,12 @@ def async_migrate_entities_unique_ids( f"{registry_entry.config_entry_id}_" ).removesuffix(f"_{registry_entry.translation_key}") if monitor := next( - (m for m in metrics.values() if m.monitor_name == name), None + ( + m + for m in metrics.values() + if m.monitor_name == name and m.monitor_id is not None + ), + None, ): entity_registry.async_update_entity( registry_entry.entity_id, From beca01e8571d9caac9cc2d1b70ed59105cfd1990 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 7 Aug 2025 19:43:36 +0200 Subject: [PATCH 138/247] Silence vacuum battery deprecation for built in integrations (#150204) --- homeassistant/components/vacuum/__init__.py | 4 +- tests/components/vacuum/test_init.py | 103 +++++++++++--------- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 11db9108db33a..eb8789779a747 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -333,7 +333,7 @@ def _report_deprecated_battery_properties(self, property: str) -> None: f"is setting the {property} which has been deprecated." f" Integration {self.platform.platform_name} should implement a sensor" " instead with a correct device class and link it to the same device", - core_integration_behavior=ReportBehavior.LOG, + core_integration_behavior=ReportBehavior.IGNORE, custom_integration_behavior=ReportBehavior.LOG, breaks_in_ha_version="2026.8", integration_domain=self.platform.platform_name, @@ -358,7 +358,7 @@ def _report_deprecated_battery_feature(self) -> None: f" Integration {self.platform.platform_name} should remove this as part of migrating" " the battery level and icon to a sensor", core_behavior=ReportBehavior.LOG, - core_integration_behavior=ReportBehavior.LOG, + core_integration_behavior=ReportBehavior.IGNORE, custom_integration_behavior=ReportBehavior.LOG, breaks_in_ha_version="2026.8", integration_domain=self.platform.platform_name, diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 60ff0a1ebde07..92fbca483fd15 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -3,6 +3,7 @@ from __future__ import annotations from enum import Enum +import logging from types import ModuleType from typing import Any @@ -437,11 +438,13 @@ def start(self) -> None: assert state.state == "cleaning" -@pytest.mark.usefixtures("mock_as_custom_component") -async def test_vacuum_log_deprecated_battery_properties( +@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)]) +async def test_vacuum_log_deprecated_battery_using_properties( hass: HomeAssistant, config_flow_fixture: None, caplog: pytest.LogCaptureFixture, + is_built_in: bool, + log_warnings: int, ) -> None: """Test incorrectly using battery properties logs warning.""" @@ -449,7 +452,7 @@ class MockLegacyVacuum(MockVacuum): """Mocked vacuum entity.""" @property - def activity(self) -> str: + def activity(self) -> VacuumActivity: """Return the state of the entity.""" return VacuumActivity.CLEANING @@ -477,7 +480,7 @@ def battery_icon(self) -> str: async_setup_entry=help_async_setup_entry_init, async_unload_entry=help_async_unload_entry, ), - built_in=False, + built_in=is_built_in, ) setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -486,26 +489,27 @@ def battery_icon(self) -> str: assert state is not None assert ( - "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." - " Integration test should implement a sensor instead with a correct device class and link it" - " to the same device. This will stop working in Home Assistant 2026.8," - " please report it to the author of the 'test' custom integration" - in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == log_warnings ) + assert ( - "Detected that custom integration 'test' is setting the battery_level which has been deprecated." - " Integration test should implement a sensor instead with a correct device class and link it" - " to the same device. This will stop working in Home Assistant 2026.8," - " please report it to the author of the 'test' custom integration" + "integration 'test' is setting the battery_icon which has been deprecated." in caplog.text - ) + ) != is_built_in + assert ( + "integration 'test' is setting the battery_level which has been deprecated." + in caplog.text + ) != is_built_in -@pytest.mark.usefixtures("mock_as_custom_component") -async def test_vacuum_log_deprecated_battery_properties_using_attr( +@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)]) +async def test_vacuum_log_deprecated_battery_using_attr( hass: HomeAssistant, config_flow_fixture: None, caplog: pytest.LogCaptureFixture, + is_built_in: bool, + log_warnings: int, ) -> None: """Test incorrectly using _attr_battery_* attribute does log issue and raise repair.""" @@ -531,7 +535,7 @@ def start(self) -> None: async_setup_entry=help_async_setup_entry_init, async_unload_entry=help_async_unload_entry, ), - built_in=False, + built_in=is_built_in, ) setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -541,47 +545,51 @@ def start(self) -> None: entity.start() assert ( - "Detected that custom integration 'test' is setting the battery_level which has been deprecated." - " Integration test should implement a sensor instead with a correct device class and link it to" - " the same device. This will stop working in Home Assistant 2026.8," - " please report it to the author of the 'test' custom integration" - in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == log_warnings ) + assert ( - "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." - " Integration test should implement a sensor instead with a correct device class and link it to" - " the same device. This will stop working in Home Assistant 2026.8," - " please report it to the author of the 'test' custom integration" + "integration 'test' is setting the battery_level which has been deprecated." in caplog.text - ) + ) != is_built_in + assert ( + "integration 'test' is setting the battery_icon which has been deprecated." + in caplog.text + ) != is_built_in await async_start(hass, entity.entity_id) caplog.clear() + await async_start(hass, entity.entity_id) + # Test we only log once assert ( - "Detected that custom integration 'test' is setting the battery_level which has been deprecated." - not in caplog.text - ) - assert ( - "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." - not in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == 0 ) -@pytest.mark.usefixtures("mock_as_custom_component") +@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 1)]) async def test_vacuum_log_deprecated_battery_supported_feature( hass: HomeAssistant, config_flow_fixture: None, caplog: pytest.LogCaptureFixture, + is_built_in: bool, + log_warnings: int, ) -> None: """Test incorrectly setting battery supported feature logs warning.""" - entity = MockVacuum( - name="Testing", - entity_id="vacuum.test", - ) + class MockVacuum(StateVacuumEntity): + """Mock vacuum class.""" + + _attr_supported_features = ( + VacuumEntityFeature.STATE | VacuumEntityFeature.BATTERY + ) + _attr_name = "Testing" + + entity = MockVacuum() config_entry = MockConfigEntry(domain="test") config_entry.add_to_hass(hass) @@ -592,7 +600,7 @@ async def test_vacuum_log_deprecated_battery_supported_feature( async_setup_entry=help_async_setup_entry_init, async_unload_entry=help_async_unload_entry, ), - built_in=False, + built_in=is_built_in, ) setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -601,13 +609,14 @@ async def test_vacuum_log_deprecated_battery_supported_feature( assert state is not None assert ( - "Detected that custom integration 'test' is setting the battery supported feature" - " which has been deprecated. Integration test should remove this as part of migrating" - " the battery level and icon to a sensor. This will stop working in Home Assistant 2026.8" - ", please report it to the author of the 'test' custom integration" - in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == log_warnings ) + assert ( + "integration 'test' is setting the battery supported feature" in caplog.text + ) != is_built_in + async def test_vacuum_not_log_deprecated_battery_properties_during_init( hass: HomeAssistant, @@ -624,7 +633,7 @@ def __init__(self, **kwargs: Any) -> None: self._attr_battery_level = 50 @property - def activity(self) -> str: + def activity(self) -> VacuumActivity: """Return the state of the entity.""" return VacuumActivity.CLEANING @@ -635,6 +644,6 @@ def activity(self) -> str: assert entity.battery_level == 50 assert ( - "Detected that custom integration 'test' is setting the battery_level which has been deprecated." - not in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == 0 ) From bc70aeea853789e6282f827c82fc0ec70d616ff8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:27:17 -0400 Subject: [PATCH 139/247] Bump ZHA to 0.0.68 (#150208) --- 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 9842fa7a0f352..5cad3c823b88f 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.67"], + "requirements": ["zha==0.0.68"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index e312782fbbf4a..480f9721dd243 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3203,7 +3203,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.67 +zha==0.0.68 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d09c83faf81ee..eefdefbfe3588 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2647,7 +2647,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.67 +zha==0.0.68 # homeassistant.components.zwave_js zwave-js-server-python==0.67.1 From 23619fb2d3f5c234fce22fb8000b97b3b7037079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 7 Aug 2025 18:42:22 +0100 Subject: [PATCH 140/247] Bump hass-nabucasa from 0.111.1 to 0.111.2 (#150209) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 76e55bc19b3b8..cb3537a59e5bc 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.111.1"], + "requirements": ["hass-nabucasa==0.111.2"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 816b2e453e755..ac484a5d5d000 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==4.0.2 -hass-nabucasa==0.111.1 +hass-nabucasa==0.111.2 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250806.0 diff --git a/pyproject.toml b/pyproject.toml index 1b58380670345..fb39802e5f425 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.111.1", + "hass-nabucasa==0.111.2", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", diff --git a/requirements.txt b/requirements.txt index af9a835e0d95a..7bd900a69ed05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.111.1 +hass-nabucasa==0.111.2 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 480f9721dd243..bae123d243935 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ habiticalib==0.4.1 habluetooth==4.0.2 # homeassistant.components.cloud -hass-nabucasa==0.111.1 +hass-nabucasa==0.111.2 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eefdefbfe3588..0b83a0c44d520 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ habiticalib==0.4.1 habluetooth==4.0.2 # homeassistant.components.cloud -hass-nabucasa==0.111.1 +hass-nabucasa==0.111.2 # homeassistant.components.assist_satellite # homeassistant.components.conversation From 8d821d9f988be9dfcb66444e4593ca02413142b2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:27:00 -0400 Subject: [PATCH 141/247] Fix JSON serialization for ZHA diagnostics download (#150210) --- homeassistant/components/zha/diagnostics.py | 16 +++++++++++++++- .../zha/snapshots/test_diagnostics.ambr | 1 + tests/components/zha/test_diagnostics.py | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 6c5fcba1f8b3d..4383aa52afa54 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -8,6 +8,7 @@ from zha.application.const import ATTR_IEEE from zha.application.gateway import Gateway +from zigpy.application import ControllerApplication from zigpy.config import CONF_NWK_EXTENDED_PAN_ID from zigpy.types import Channels @@ -63,6 +64,19 @@ def shallow_asdict(obj: Any) -> dict: return obj +def get_application_state_diagnostics(app: ControllerApplication) -> dict: + """Dump the application state as a dictionary.""" + data = shallow_asdict(app.state) + + # EUI64 objects in zigpy are not subclasses of any JSON-serializable key type and + # must be converted to strings. + data["network_info"]["nwk_addresses"] = { + str(k): v for k, v in data["network_info"]["nwk_addresses"].items() + } + + return data + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: @@ -79,7 +93,7 @@ async def async_get_config_entry_diagnostics( { "config": zha_data.yaml_config, "config_entry": config_entry.as_dict(), - "application_state": shallow_asdict(app.state), + "application_state": get_application_state_diagnostics(app), "energy_scan": { channel: 100 * energy / 255 for channel, energy in energy_scan.items() }, diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index 35eb320893f3f..4d90942fb9718 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -36,6 +36,7 @@ }), 'network_key': '**REDACTED**', 'nwk_addresses': dict({ + '11:22:33:44:55:66:77:88': 4660, }), 'nwk_manager_id': 0, 'nwk_update_id': 0, diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 0e78a9a1b5bb6..d32dd19152711 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -6,6 +6,7 @@ from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from zigpy.profiles import zha +from zigpy.types import EUI64, NWK from zigpy.zcl.clusters import security from homeassistant.components.zha.helpers import ( @@ -71,6 +72,10 @@ async def test_diagnostics_for_config_entry( gateway.application_controller.energy_scan.side_effect = None gateway.application_controller.energy_scan.return_value = scan + gateway.application_controller.state.network_info.nwk_addresses = { + EUI64.convert("11:22:33:44:55:66:77:88"): NWK(0x1234) + } + diagnostics_data = await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) From 3ef332e1687cdbf7312dee6d653419aca2669ba6 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 7 Aug 2025 20:11:39 +0200 Subject: [PATCH 142/247] Ignore MQTT vacuum battery warning (#150211) --- homeassistant/components/vacuum/__init__.py | 5 ++++- tests/components/mqtt/test_vacuum.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index eb8789779a747..081b7a15995f6 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -79,7 +79,10 @@ _DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1") _DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1") -_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",) +_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ( + "mqtt", + "template", +) class VacuumEntityFeature(IntFlag): diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 77b90403823c3..b0c5981fbe12d 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -2,6 +2,7 @@ from copy import deepcopy import json +import logging from typing import Any from unittest.mock import patch @@ -395,6 +396,15 @@ async def test_status_with_deprecated_battery_feature( assert issue.issue_domain == "vacuum" assert issue.translation_key == "deprecated_vacuum_battery_feature" assert issue.translation_placeholders == {"entity_id": "vacuum.mqtttest"} + assert not [ + record + for record in caplog.records + if record.name == "homeassistant.helpers.frame" + and record.levelno >= logging.WARNING + ] + assert ( + "mqtt' is setting the battery_level which has been deprecated" + ) not in caplog.text @pytest.mark.parametrize( From 8afe3fed7467f67fdc2251aee26ebc6d91e289f7 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:26:02 +0200 Subject: [PATCH 143/247] Handle Unifi Protect BadRequest exception during API key creation (#150223) --- .../components/unifiprotect/__init__.py | 4 +-- tests/components/unifiprotect/test_init.py | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 5fa9a85d3418b..97a5ca6718615 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -8,7 +8,7 @@ from aiohttp.client_exceptions import ServerDisconnectedError from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import Bootstrap -from uiprotect.exceptions import ClientError, NotAuthorized +from uiprotect.exceptions import BadRequest, ClientError, NotAuthorized # Import the test_util.anonymize module from the uiprotect package # in __init__ to ensure it gets imported in the executor since the @@ -100,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: new_api_key = await protect.create_api_key( name=f"Home Assistant ({hass.config.location_name})" ) - except NotAuthorized as err: + except (NotAuthorized, BadRequest) as err: _LOGGER.error("Failed to create API key: %s", err) else: protect.set_api_key(new_api_key) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index b951d95fbdc9f..0776feece542c 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -5,9 +5,10 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect import NvrError, ProtectApiClient from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import NVR, Bootstrap, CloudAccount, Light +from uiprotect.exceptions import BadRequest, NotAuthorized from homeassistant.components.unifiprotect.const import ( AUTH_RETRIES, @@ -414,6 +415,28 @@ async def test_setup_handles_api_key_creation_failure( ufp.api.set_api_key.assert_not_called() +@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True) +async def test_setup_handles_api_key_creation_bad_request( + hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock +) -> None: + """Test handling of API key creation BadRequest error.""" + # Setup: API key is not set, user has write permissions, but creation fails with BadRequest + ufp.api.is_api_key_set.return_value = False + ufp.api.create_api_key = AsyncMock( + side_effect=BadRequest("Invalid API key creation request") + ) + + # Should fail with auth error due to API key creation failure + await hass.config_entries.async_setup(ufp.entry.entry_id) + await hass.async_block_till_done() + + assert ufp.entry.state is ConfigEntryState.SETUP_ERROR + + # Verify API key creation was attempted but set_api_key was not called + ufp.api.create_api_key.assert_called_once_with(name="Home Assistant (test home)") + ufp.api.set_api_key.assert_not_called() + + async def test_setup_with_existing_api_key( hass: HomeAssistant, ufp: MockUFPFixture ) -> None: From 90c03f41152b3b9617dc5c7553a740b90994e7f6 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 7 Aug 2025 22:39:24 +0200 Subject: [PATCH 144/247] Fix Tibber coordinator ContextVar warning (#150229) --- homeassistant/components/tibber/sensor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 327812cdf999d..1c56d5b2ce6eb 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -299,7 +299,10 @@ async def async_setup_entry( ) await home.rt_subscribe( TibberRtDataCoordinator( - entity_creator.add_sensors, home, hass + hass, + entry, + entity_creator.add_sensors, + home, ).async_set_updated_data ) @@ -613,15 +616,17 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en def __init__( self, + hass: HomeAssistant, + config_entry: ConfigEntry, add_sensor_callback: Callable[[TibberRtDataCoordinator, Any], None], tibber_home: tibber.TibberHome, - hass: HomeAssistant, ) -> None: """Initialize the data handler.""" self._add_sensor_callback = add_sensor_callback super().__init__( hass, _LOGGER, + config_entry=config_entry, name=tibber_home.info["viewer"]["home"]["address"].get( "address1", "Tibber" ), From a2931efeebcef1e1adbb1f2bc00993e6859303df Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Fri, 8 Aug 2025 13:26:04 +0100 Subject: [PATCH 145/247] Fix handing for zero volume error in Squeezebox (#150265) --- homeassistant/components/squeezebox/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 49aad4fd6984a..839e419dd96cb 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -325,7 +325,7 @@ async def async_will_remove_from_hass(self) -> None: @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - if self._player.volume: + if self._player.volume is not None: return int(float(self._player.volume)) / 100.0 return None From 66019953dbb3a72e8f8db31545259e1484071224 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:24:41 +0100 Subject: [PATCH 146/247] Fix error on startup when no Apps or Radio plugins are installed for Squeezebox (#150267) --- .../components/squeezebox/browse_media.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 4f2a1fa7aa570..e14f1989cbe18 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -157,26 +157,28 @@ async def async_init(self, player: Player, browse_limit: int) -> None: cmd = ["apps", 0, browse_limit] result = await player.async_query(*cmd) - for app in result["appss_loop"]: - app_cmd = "app-" + app["cmd"] - if app_cmd not in self.known_apps_radios: - self.add_new_command(app_cmd, "item_id") - _LOGGER.debug( - "Adding new command %s to browse data for player %s", - app_cmd, - player.player_id, - ) + if result["appss_loop"]: + for app in result["appss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) cmd = ["radios", 0, browse_limit] result = await player.async_query(*cmd) - for app in result["radioss_loop"]: - app_cmd = "app-" + app["cmd"] - if app_cmd not in self.known_apps_radios: - self.add_new_command(app_cmd, "item_id") - _LOGGER.debug( - "Adding new command %s to browse data for player %s", - app_cmd, - player.player_id, - ) + if result["radioss_loop"]: + for app in result["radioss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) def _build_response_apps_radios_category( From 762c179b803c209f899c7d189495372134db0556 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:25:19 +0200 Subject: [PATCH 147/247] Volvo: fix missing charging power options (#150272) --- homeassistant/components/volvo/sensor.py | 7 +++- homeassistant/components/volvo/strings.json | 4 +- .../fixtures/ex30_2024/energy_state.json | 41 +++++++++---------- .../volvo/snapshots/test_sensor.ambr | 24 +++++++---- 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index dd982238a47cc..647c7b578e88a 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -87,7 +87,12 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: return None -_CHARGING_POWER_STATUS_OPTIONS = ["providing_power", "no_power_available"] +_CHARGING_POWER_STATUS_OPTIONS = [ + "fault", + "power_available_but_not_activated", + "providing_power", + "no_power_available", +] _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( # command-accessibility endpoint diff --git a/homeassistant/components/volvo/strings.json b/homeassistant/components/volvo/strings.json index 4fe7429117ccd..c429c10657403 100644 --- a/homeassistant/components/volvo/strings.json +++ b/homeassistant/components/volvo/strings.json @@ -94,7 +94,7 @@ "state": { "connected": "[%key:common::state::connected%]", "disconnected": "[%key:common::state::disconnected%]", - "fault": "[%key:common::state::error%]" + "fault": "[%key:common::state::fault%]" } }, "charging_current_limit": { @@ -106,6 +106,8 @@ "charging_power_status": { "name": "Charging power status", "state": { + "fault": "[%key:common::state::fault%]", + "power_available_but_not_activated": "Power available", "providing_power": "Providing power", "no_power_available": "No power" } diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_state.json b/tests/components/volvo/fixtures/ex30_2024/energy_state.json index fe42dba568a3f..0170d1aa617f3 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_state.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_state.json @@ -1,57 +1,56 @@ { "batteryChargeLevel": { "status": "OK", - "value": 38, + "value": 90.0, "unit": "percentage", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "electricRange": { "status": "OK", - "value": 90, + "value": 327, "unit": "km", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "chargerConnectionStatus": { "status": "OK", - "value": "DISCONNECTED", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "CONNECTED", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingStatus": { "status": "OK", - "value": "IDLE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "DONE", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingType": { "status": "OK", - "value": "NONE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "AC", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargerPowerStatus": { "status": "OK", - "value": "NO_POWER_AVAILABLE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "FAULT", + "updatedAt": "2025-08-07T14:30:32Z" }, "estimatedChargingTimeToTargetBatteryChargeLevel": { "status": "OK", - "value": 0, + "value": 2, "unit": "minutes", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingCurrentLimit": { - "status": "OK", - "value": 32, - "unit": "ampere", - "updatedAt": "2024-03-05T08:38:44Z" + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" }, "targetBatteryChargeLevel": { "status": "OK", "value": 90, "unit": "percentage", - "updatedAt": "2024-09-22T09:40:12Z" + "updatedAt": "2025-08-07T14:49:50Z" }, "chargingPower": { "status": "ERROR", - "code": "PROPERTY_NOT_FOUND", - "message": "No valid value could be found for the requested property" + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" } } diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index d5346cf9cd8b9..b651bbd526f1e 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -52,7 +52,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '38', + 'state': '90.0', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_battery_capacity-entry] @@ -229,7 +229,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'disconnected', + 'state': 'connected', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_limit-entry] @@ -285,7 +285,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '32', + 'state': 'unavailable', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-entry] @@ -351,6 +351,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -390,6 +392,8 @@ 'device_class': 'enum', 'friendly_name': 'Volvo EX30 Charging power status', 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -399,7 +403,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'no_power_available', + 'state': 'fault', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_status-entry] @@ -465,7 +469,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'idle', + 'state': 'done', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_type-entry] @@ -525,7 +529,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'none', + 'state': 'ac', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_empty_battery-entry] @@ -581,7 +585,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '90', + 'state': '327', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_service-entry] @@ -693,7 +697,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '2', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_odometer-entry] @@ -2276,6 +2280,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -2315,6 +2321,8 @@ 'device_class': 'enum', 'friendly_name': 'Volvo XC40 Charging power status', 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), From 0c31ec9bb6f5ae22a94e452a95c40dbadbd79607 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 8 Aug 2025 15:06:31 +0200 Subject: [PATCH 148/247] Constraint num2words to 0.5.14 (#150276) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ac484a5d5d000..690722545371c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -213,3 +213,6 @@ multidict>=6.4.2 # Stable Alpine current only ships cargo 1.83.0 # No wheels upstream available for armhf & armv7 rpds-py==0.24.0 + +# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI +num2words==0.5.14 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 13bb33842588a..779393d2c79a6 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -239,6 +239,9 @@ # Stable Alpine current only ships cargo 1.83.0 # No wheels upstream available for armhf & armv7 rpds-py==0.24.0 + +# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI +num2words==0.5.14 """ GENERATED_MESSAGE = ( From a1731cd21029dc3d75bc20e26f9eef860d7c6013 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:28:29 +0200 Subject: [PATCH 149/247] Volvo: fix distance to empty battery (#150278) --- homeassistant/components/volvo/sensor.py | 20 +++++++++---------- .../xc40_electric_2024/energy_state.json | 4 ++-- .../volvo/snapshots/test_sensor.ambr | 4 ++-- tests/components/volvo/test_sensor.py | 16 +++++++++++++++ 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index 647c7b578e88a..caadebb6e2a24 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass, replace +from dataclasses import dataclass import logging from typing import Any, cast @@ -47,7 +47,6 @@ class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription): """Describes a Volvo sensor entity.""" - source_fields: list[str] | None = None value_fn: Callable[[VolvoCarsValue], Any] | None = None @@ -240,11 +239,15 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: "none", ], ), - # statistics & energy state endpoint + # statistics endpoint + # We're not using `electricRange` from the energy state endpoint because + # the official app seems to use `distanceToEmptyBattery`. + # In issue #150213, a user described to behavior as follows: + # - For a `distanceToEmptyBattery` of 250km, the `electricRange` was 150mi + # - For a `distanceToEmptyBattery` of 260km, the `electricRange` was 160mi VolvoSensorDescription( key="distance_to_empty_battery", - api_field="", - source_fields=["distanceToEmptyBattery", "electricRange"], + api_field="distanceToEmptyBattery", native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, @@ -362,12 +365,7 @@ def _add_entity( if description.key in added_keys: continue - if description.source_fields: - for field in description.source_fields: - if field in coordinator.data: - description = replace(description, api_field=field) - _add_entity(coordinator, description) - elif description.api_field in coordinator.data: + if description.api_field in coordinator.data: _add_entity(coordinator, description) async_add_entities(entities) diff --git a/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json b/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json index 16208571c4783..bac596857b015 100644 --- a/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json +++ b/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json @@ -7,8 +7,8 @@ }, "electricRange": { "status": "OK", - "value": 220, - "unit": "km", + "value": 150, + "unit": "mi", "updatedAt": "2025-07-02T08:51:23Z" }, "chargerConnectionStatus": { diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index b651bbd526f1e..53e05c49c970b 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -585,7 +585,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '327', + 'state': '250', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_service-entry] @@ -2514,7 +2514,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '220', + 'state': '250', }) # --- # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_distance_to_service-entry] diff --git a/tests/components/volvo/test_sensor.py b/tests/components/volvo/test_sensor.py index f610ee2ed5731..e4cc69470aead 100644 --- a/tests/components/volvo/test_sensor.py +++ b/tests/components/volvo/test_sensor.py @@ -30,3 +30,19 @@ async def test_sensor( assert await setup_integration() await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "full_model", + ["xc40_electric_2024"], +) +async def test_distance_to_empty_battery( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], +) -> None: + """Test using `distanceToEmptyBattery` instead of `electricRange`.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): + assert await setup_integration() + + assert hass.states.get("sensor.volvo_xc40_distance_to_empty_battery").state == "250" From 3d39fb08e53471cf8045b707a564143770754078 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Fri, 8 Aug 2025 21:49:09 +0300 Subject: [PATCH 150/247] Add GPT-5 support (#150281) --- .../openai_conversation/config_flow.py | 26 ++++++++++- .../components/openai_conversation/const.py | 2 + .../components/openai_conversation/entity.py | 12 +++-- .../openai_conversation/strings.json | 8 ++++ .../openai_conversation/conftest.py | 2 +- .../openai_conversation/test_config_flow.py | 46 +++++++++++++------ 6 files changed, 77 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index c45c2b997b3f1..0b2fa75b5c07a 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -49,6 +49,7 @@ CONF_RECOMMENDED, CONF_TEMPERATURE, CONF_TOP_P, + CONF_VERBOSITY, CONF_WEB_SEARCH, CONF_WEB_SEARCH_CITY, CONF_WEB_SEARCH_CONTEXT_SIZE, @@ -67,6 +68,7 @@ RECOMMENDED_REASONING_EFFORT, RECOMMENDED_TEMPERATURE, RECOMMENDED_TOP_P, + RECOMMENDED_VERBOSITY, RECOMMENDED_WEB_SEARCH, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE, RECOMMENDED_WEB_SEARCH_USER_LOCATION, @@ -323,7 +325,7 @@ async def async_step_model( model = options[CONF_CHAT_MODEL] - if model.startswith("o"): + if model.startswith(("o", "gpt-5")): step_schema.update( { vol.Optional( @@ -331,7 +333,9 @@ async def async_step_model( default=RECOMMENDED_REASONING_EFFORT, ): SelectSelector( SelectSelectorConfig( - options=["low", "medium", "high"], + options=["low", "medium", "high"] + if model.startswith("o") + else ["minimal", "low", "medium", "high"], translation_key=CONF_REASONING_EFFORT, mode=SelectSelectorMode.DROPDOWN, ) @@ -341,6 +345,24 @@ async def async_step_model( elif CONF_REASONING_EFFORT in options: options.pop(CONF_REASONING_EFFORT) + if model.startswith("gpt-5"): + step_schema.update( + { + vol.Optional( + CONF_VERBOSITY, + default=RECOMMENDED_VERBOSITY, + ): SelectSelector( + SelectSelectorConfig( + options=["low", "medium", "high"], + translation_key=CONF_VERBOSITY, + mode=SelectSelectorMode.DROPDOWN, + ) + ), + } + ) + elif CONF_VERBOSITY in options: + options.pop(CONF_VERBOSITY) + if self._subentry_type == "conversation" and not model.startswith( tuple(UNSUPPORTED_WEB_SEARCH_MODELS) ): diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index cacef6fcff92b..2fd189132074f 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -21,6 +21,7 @@ CONF_RECOMMENDED = "recommended" CONF_TEMPERATURE = "temperature" CONF_TOP_P = "top_p" +CONF_VERBOSITY = "verbosity" CONF_WEB_SEARCH = "web_search" CONF_WEB_SEARCH_USER_LOCATION = "user_location" CONF_WEB_SEARCH_CONTEXT_SIZE = "search_context_size" @@ -34,6 +35,7 @@ RECOMMENDED_REASONING_EFFORT = "low" RECOMMENDED_TEMPERATURE = 1.0 RECOMMENDED_TOP_P = 1.0 +RECOMMENDED_VERBOSITY = "medium" RECOMMENDED_WEB_SEARCH = False RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE = "medium" RECOMMENDED_WEB_SEARCH_USER_LOCATION = False diff --git a/homeassistant/components/openai_conversation/entity.py b/homeassistant/components/openai_conversation/entity.py index c1b2f970f07f8..748c0c8f87441 100644 --- a/homeassistant/components/openai_conversation/entity.py +++ b/homeassistant/components/openai_conversation/entity.py @@ -61,6 +61,7 @@ CONF_REASONING_EFFORT, CONF_TEMPERATURE, CONF_TOP_P, + CONF_VERBOSITY, CONF_WEB_SEARCH, CONF_WEB_SEARCH_CITY, CONF_WEB_SEARCH_CONTEXT_SIZE, @@ -75,6 +76,7 @@ RECOMMENDED_REASONING_EFFORT, RECOMMENDED_TEMPERATURE, RECOMMENDED_TOP_P, + RECOMMENDED_VERBOSITY, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE, ) @@ -346,14 +348,18 @@ async def _async_handle_chat_log( if tools: model_args["tools"] = tools - if model_args["model"].startswith("o"): + if model_args["model"].startswith(("o", "gpt-5")): model_args["reasoning"] = { "effort": options.get( CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT ) } - else: - model_args["store"] = False + model_args["include"] = ["reasoning.encrypted_content"] + + if model_args["model"].startswith("gpt-5"): + model_args["text"] = { + "verbosity": options.get(CONF_VERBOSITY, RECOMMENDED_VERBOSITY) + } messages = [ m diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index a1bf236f19b7f..304ef8b6bdcc8 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -121,6 +121,7 @@ "selector": { "reasoning_effort": { "options": { + "minimal": "Minimal", "low": "[%key:common::state::low%]", "medium": "[%key:common::state::medium%]", "high": "[%key:common::state::high%]" @@ -132,6 +133,13 @@ "medium": "[%key:common::state::medium%]", "high": "[%key:common::state::high%]" } + }, + "verbosity": { + "options": { + "low": "[%key:common::state::low%]", + "medium": "[%key:common::state::medium%]", + "high": "[%key:common::state::high%]" + } } }, "services": { diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index b58e6c31f381b..38d8967e6c53b 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -94,7 +94,7 @@ def mock_config_entry_with_reasoning_model( hass.config_entries.async_update_subentry( mock_config_entry, next(iter(mock_config_entry.subentries.values())), - data={CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_CHAT_MODEL: "o4-mini"}, + data={CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_CHAT_MODEL: "gpt-5-mini"}, ) return mock_config_entry diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 6d8fb143f88c9..3f3b7801c8f34 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -20,6 +20,7 @@ CONF_RECOMMENDED, CONF_TEMPERATURE, CONF_TOP_P, + CONF_VERBOSITY, CONF_WEB_SEARCH, CONF_WEB_SEARCH_CITY, CONF_WEB_SEARCH_CONTEXT_SIZE, @@ -302,7 +303,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non ( { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pirate", + CONF_PROMPT: "Speak like a pro", }, { CONF_TEMPERATURE: 1.0, @@ -317,7 +318,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non ), { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pirate", + CONF_PROMPT: "Speak like a pro", CONF_TEMPERATURE: 1.0, CONF_CHAT_MODEL: "o1-pro", CONF_TOP_P: RECOMMENDED_TOP_P, @@ -414,35 +415,51 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non ( # Case 2: reasoning model { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pro", + CONF_PROMPT: "Speak like a pirate", CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "o1-pro", + CONF_CHAT_MODEL: "gpt-5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, - CONF_REASONING_EFFORT: "high", + CONF_REASONING_EFFORT: "low", + CONF_VERBOSITY: "high", + CONF_CODE_INTERPRETER: False, + CONF_WEB_SEARCH: False, + CONF_WEB_SEARCH_CONTEXT_SIZE: "low", + CONF_WEB_SEARCH_USER_LOCATION: False, }, ( { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pro", + CONF_PROMPT: "Speak like a pirate", }, { CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "o1-pro", + CONF_CHAT_MODEL: "gpt-5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, }, - {CONF_REASONING_EFFORT: "high", CONF_CODE_INTERPRETER: False}, + { + CONF_REASONING_EFFORT: "minimal", + CONF_CODE_INTERPRETER: False, + CONF_VERBOSITY: "high", + CONF_WEB_SEARCH: False, + CONF_WEB_SEARCH_CONTEXT_SIZE: "low", + CONF_WEB_SEARCH_USER_LOCATION: False, + }, ), { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pro", + CONF_PROMPT: "Speak like a pirate", CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "o1-pro", + CONF_CHAT_MODEL: "gpt-5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, - CONF_REASONING_EFFORT: "high", + CONF_REASONING_EFFORT: "minimal", CONF_CODE_INTERPRETER: False, + CONF_VERBOSITY: "high", + CONF_WEB_SEARCH: False, + CONF_WEB_SEARCH_CONTEXT_SIZE: "low", + CONF_WEB_SEARCH_USER_LOCATION: False, }, ), # Test that old options are removed after reconfiguration @@ -482,11 +499,13 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non CONF_PROMPT: "Speak like a pirate", CONF_LLM_HASS_API: ["assist"], CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "gpt-4o", + CONF_CHAT_MODEL: "gpt-5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, CONF_REASONING_EFFORT: "high", CONF_CODE_INTERPRETER: True, + CONF_VERBOSITY: "low", + CONF_WEB_SEARCH: False, }, ( { @@ -550,11 +569,12 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non CONF_PROMPT: "Speak like a pirate", CONF_LLM_HASS_API: ["assist"], CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "o3-mini", + CONF_CHAT_MODEL: "o5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, CONF_REASONING_EFFORT: "low", CONF_CODE_INTERPRETER: True, + CONF_VERBOSITY: "medium", }, ( { From 39f41fe17da4d961fa8eedbba4557e3a79c18c01 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:24:53 +0200 Subject: [PATCH 151/247] Volvo: Skip unsupported API fields (#150285) --- homeassistant/components/volvo/coordinator.py | 19 +- tests/components/volvo/__init__.py | 6 + .../xc60_phev_2020/energy_capabilities.json | 33 + .../fixtures/xc60_phev_2020/energy_state.json | 52 + .../fixtures/xc60_phev_2020/statistics.json | 32 + .../fixtures/xc60_phev_2020/vehicle.json | 17 + .../volvo/snapshots/test_sensor.ambr | 1026 +++++++++++++++-- tests/components/volvo/test_sensor.py | 25 +- 8 files changed, 1096 insertions(+), 114 deletions(-) create mode 100644 tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json create mode 100644 tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json create mode 100644 tests/components/volvo/fixtures/xc60_phev_2020/statistics.json create mode 100644 tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json diff --git a/homeassistant/components/volvo/coordinator.py b/homeassistant/components/volvo/coordinator.py index 8ddaaee078129..da23e7875c917 100644 --- a/homeassistant/components/volvo/coordinator.py +++ b/homeassistant/components/volvo/coordinator.py @@ -15,6 +15,7 @@ VolvoAuthException, VolvoCarsApiBaseModel, VolvoCarsValue, + VolvoCarsValueStatusField, VolvoCarsVehicle, ) @@ -36,6 +37,16 @@ type CoordinatorData = dict[str, VolvoCarsApiBaseModel | None] +def _is_invalid_api_field(field: VolvoCarsApiBaseModel | None) -> bool: + if not field: + return True + + if isinstance(field, VolvoCarsValueStatusField) and field.status == "ERROR": + return True + + return False + + class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]): """Volvo base coordinator.""" @@ -121,7 +132,13 @@ async def _async_update_data(self) -> CoordinatorData: translation_key="update_failed", ) from result - data |= cast(CoordinatorData, result) + api_data = cast(CoordinatorData, result) + data |= { + key: field + for key, field in api_data.items() + if not _is_invalid_api_field(field) + } + valid = True # Raise an error if not a single API call succeeded diff --git a/tests/components/volvo/__init__.py b/tests/components/volvo/__init__.py index 875052fcf7ef3..acd608b8d2626 100644 --- a/tests/components/volvo/__init__.py +++ b/tests/components/volvo/__init__.py @@ -20,6 +20,12 @@ "statistics", "vehicle", ], + "xc60_phev_2020": [ + "energy_capabilities", + "energy_state", + "statistics", + "vehicle", + ], "xc90_petrol_2019": ["commands", "statistics", "vehicle"], } diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json new file mode 100644 index 0000000000000..d8aa07ff0bb65 --- /dev/null +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json @@ -0,0 +1,33 @@ +{ + "isSupported": true, + "batteryChargeLevel": { + "isSupported": false + }, + "electricRange": { + "isSupported": false + }, + "chargerConnectionStatus": { + "isSupported": true + }, + "chargingSystemStatus": { + "isSupported": true + }, + "chargingType": { + "isSupported": false + }, + "chargerPowerStatus": { + "isSupported": false + }, + "estimatedChargingTimeToTargetBatteryChargeLevel": { + "isSupported": false + }, + "targetBatteryChargeLevel": { + "isSupported": true + }, + "chargingCurrentLimit": { + "isSupported": false + }, + "chargingPower": { + "isSupported": false + } +} diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json new file mode 100644 index 0000000000000..e2f0cd13807b0 --- /dev/null +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json @@ -0,0 +1,52 @@ +{ + "batteryChargeLevel": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "electricRange": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "chargerConnectionStatus": { + "status": "OK", + "value": "DISCONNECTED", + "updatedAt": "2025-08-07T20:29:18Z" + }, + "chargingStatus": { + "status": "OK", + "value": "IDLE", + "updatedAt": "2025-08-07T20:29:18Z" + }, + "chargingType": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "chargerPowerStatus": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "estimatedChargingTimeToTargetBatteryChargeLevel": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "chargingCurrentLimit": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "targetBatteryChargeLevel": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "chargingPower": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + } +} diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/statistics.json b/tests/components/volvo/fixtures/xc60_phev_2020/statistics.json new file mode 100644 index 0000000000000..91384f2d13ef0 --- /dev/null +++ b/tests/components/volvo/fixtures/xc60_phev_2020/statistics.json @@ -0,0 +1,32 @@ +{ + "averageFuelConsumption": { + "value": 4.0, + "unit": "l/100km", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "averageSpeed": { + "value": 65, + "unit": "km/h", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "tripMeterManual": { + "value": 219.7, + "unit": "km", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "tripMeterAutomatic": { + "value": 0.0, + "unit": "km", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "distanceToEmptyTank": { + "value": 920, + "unit": "km", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "distanceToEmptyBattery": { + "value": 29, + "unit": "km", + "timestamp": "2025-08-07T20:29:18.343Z" + } +} diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json b/tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json new file mode 100644 index 0000000000000..734672eb59ed2 --- /dev/null +++ b/tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json @@ -0,0 +1,17 @@ +{ + "vin": "YV1ABCDEFG1234567", + "modelYear": 2020, + "gearbox": "AUTOMATIC", + "fuelType": "PETROL/ELECTRIC", + "externalColour": "Bright Silver", + "batteryCapacityKWH": 11.832, + "images": { + "exteriorImageUrl": "https://cas.volvocars.com/image/dynamic/MY20_0000/123/exterior-v1/_/default.png?market=se&client=public-api-engineering&angle=1&bg=00000000&w=1920", + "internalImageUrl": "https://cas.volvocars.com/image/dynamic/MY20_0000/123/interior-v1/_/default.jpg?market=se&client=public-api-engineering&angle=0&w=1920" + }, + "descriptions": { + "model": "XC60", + "upholstery": "CHARCOAL/LEABR3/CHARC/SPO", + "steering": "LEFT" + } +} diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index 53e05c49c970b..6204a194e5128 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -232,118 +232,6 @@ 'state': 'connected', }) # --- -# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_limit-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.volvo_ex30_charging_limit', - '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': 'Charging limit', - 'platform': 'volvo', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'charging_current_limit', - 'unique_id': 'yv1abcdefg1234567_charging_current_limit', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_limit-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Volvo EX30 Charging limit', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.volvo_ex30_charging_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_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.volvo_ex30_charging_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Charging power', - 'platform': 'volvo', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'charging_power', - 'unique_id': 'yv1abcdefg1234567_charging_power', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Volvo EX30 Charging power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.volvo_ex30_charging_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3125,6 +3013,920 @@ 'state': '3822.9', }) # --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'yv1abcdefg1234567_battery_charge_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Volvo XC60 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '87.3', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_battery_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.volvo_xc60_battery_capacity', + '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': 'Battery capacity', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_capacity', + 'unique_id': 'yv1abcdefg1234567_battery_capacity', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_battery_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Volvo XC60 Battery capacity', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_battery_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.832', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_car_connection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'available', + 'car_in_use', + 'no_internet', + 'ota_installation_in_progress', + 'power_saving_mode', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_car_connection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Car connection', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'availability', + 'unique_id': 'yv1abcdefg1234567_availability', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_car_connection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC60 Car connection', + 'options': list([ + 'available', + 'car_in_use', + 'no_internet', + 'ota_installation_in_progress', + 'power_saving_mode', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_car_connection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'available', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_charging_connection_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'connected', + 'disconnected', + 'fault', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_charging_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging connection status', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charger_connection_status', + 'unique_id': 'yv1abcdefg1234567_charger_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_charging_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC60 Charging connection status', + 'options': list([ + 'connected', + 'disconnected', + 'fault', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_charging_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disconnected', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charging', + 'discharging', + 'done', + 'error', + 'idle', + 'scheduled', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'yv1abcdefg1234567_charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC60 Charging status', + 'options': list([ + 'charging', + 'discharging', + 'done', + 'error', + 'idle', + 'scheduled', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_distance_to_empty_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance to empty battery', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'distance_to_empty_battery', + 'unique_id': 'yv1abcdefg1234567_distance_to_empty_battery', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Distance to empty battery', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_distance_to_empty_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_tank-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.volvo_xc60_distance_to_empty_tank', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance to empty tank', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'distance_to_empty_tank', + 'unique_id': 'yv1abcdefg1234567_distance_to_empty_tank', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_tank-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Distance to empty tank', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_distance_to_empty_tank', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '920', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_service-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.volvo_xc60_distance_to_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance to service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'distance_to_service', + 'unique_id': 'yv1abcdefg1234567_distance_to_service', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Distance to service', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_distance_to_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29000', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_fuel_amount-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.volvo_xc60_fuel_amount', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fuel amount', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fuel_amount', + 'unique_id': 'yv1abcdefg1234567_fuel_amount', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_fuel_amount-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_storage', + 'friendly_name': 'Volvo XC60 Fuel amount', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_fuel_amount', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '47.3', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_odometer-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.volvo_xc60_odometer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Odometer', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'odometer', + 'unique_id': 'yv1abcdefg1234567_odometer', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_odometer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Odometer', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_odometer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30000', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-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.volvo_xc60_time_to_engine_service', + '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': 'Time to engine service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'engine_time_to_service', + 'unique_id': 'yv1abcdefg1234567_engine_time_to_service', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Volvo XC60 Time to engine service', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_time_to_engine_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1266', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_service-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.volvo_xc60_time_to_service', + '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': 'Time to service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_to_service', + 'unique_id': 'yv1abcdefg1234567_time_to_service', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Volvo XC60 Time to service', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_time_to_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '690', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_automatic_distance-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.volvo_xc60_trip_automatic_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Trip automatic distance', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'trip_meter_automatic', + 'unique_id': 'yv1abcdefg1234567_trip_meter_automatic', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_automatic_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Trip automatic distance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_trip_automatic_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_average_fuel_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_trip_manual_average_fuel_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Trip manual average fuel consumption', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'average_fuel_consumption', + 'unique_id': 'yv1abcdefg1234567_average_fuel_consumption', + 'unit_of_measurement': 'L/100 km', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_average_fuel_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC60 Trip manual average fuel consumption', + 'state_class': , + 'unit_of_measurement': 'L/100 km', + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_trip_manual_average_fuel_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.0', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_average_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_trip_manual_average_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Trip manual average speed', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'average_speed', + 'unique_id': 'yv1abcdefg1234567_average_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_average_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speed', + 'friendly_name': 'Volvo XC60 Trip manual average speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_trip_manual_average_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_distance-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.volvo_xc60_trip_manual_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Trip manual distance', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'trip_meter_manual', + 'unique_id': 'yv1abcdefg1234567_trip_meter_manual', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Trip manual distance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_trip_manual_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '219.7', + }) +# --- # name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/volvo/test_sensor.py b/tests/components/volvo/test_sensor.py index e4cc69470aead..2813c74128625 100644 --- a/tests/components/volvo/test_sensor.py +++ b/tests/components/volvo/test_sensor.py @@ -15,7 +15,13 @@ @pytest.mark.parametrize( "full_model", - ["ex30_2024", "s90_diesel_2018", "xc40_electric_2024", "xc90_petrol_2019"], + [ + "ex30_2024", + "s90_diesel_2018", + "xc40_electric_2024", + "xc60_phev_2020", + "xc90_petrol_2019", + ], ) async def test_sensor( hass: HomeAssistant, @@ -46,3 +52,20 @@ async def test_distance_to_empty_battery( assert await setup_integration() assert hass.states.get("sensor.volvo_xc40_distance_to_empty_battery").state == "250" + + +@pytest.mark.parametrize( + ("full_model", "short_model"), + [("ex30_2024", "ex30"), ("xc60_phev_2020", "xc60")], +) +async def test_skip_invalid_api_fields( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], + short_model: str, +) -> None: + """Test if invalid values are not creating a sensor.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): + assert await setup_integration() + + assert not hass.states.get(f"sensor.volvo_{short_model}_charging_power") From 91b10fb6d783b068b678267ef2745bde78973463 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 8 Aug 2025 20:50:14 +0200 Subject: [PATCH 152/247] Remove misleading "the" from Launch Library configuration (#150288) --- homeassistant/components/launch_library/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/launch_library/strings.json b/homeassistant/components/launch_library/strings.json index a587544f836b6..219d71600bced 100644 --- a/homeassistant/components/launch_library/strings.json +++ b/homeassistant/components/launch_library/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Do you want to configure the Launch Library?" + "description": "Do you want to configure Launch Library?" } } }, From fde548b825395246225af5de68d0aa83478d59fa Mon Sep 17 00:00:00 2001 From: steinmn <46349253+steinmn@users.noreply.github.com> Date: Sat, 9 Aug 2025 07:48:49 +0200 Subject: [PATCH 153/247] Set suggested display precision on Volvo energy/fuel consumption sensors (#150296) --- homeassistant/components/volvo/sensor.py | 5 +++++ tests/components/volvo/snapshots/test_sensor.ambr | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index caadebb6e2a24..a067549f0682a 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -114,6 +114,7 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: api_field="averageEnergyConsumption", native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -121,6 +122,7 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: api_field="averageEnergyConsumptionAutomatic", native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -128,6 +130,7 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: api_field="averageEnergyConsumptionSinceCharge", native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -135,6 +138,7 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: api_field="averageFuelConsumption", native_unit_of_measurement="L/100 km", state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -142,6 +146,7 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: api_field="averageFuelConsumptionAutomatic", native_unit_of_measurement="L/100 km", state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index 6204a194e5128..29e7e1e72a5cf 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -944,6 +944,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, @@ -1676,6 +1679,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, @@ -2873,6 +2879,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, @@ -4519,6 +4528,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, From a88549315c7ac0f0e8a52c3e2969528c5dadfb01 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 9 Aug 2025 07:48:05 +0200 Subject: [PATCH 154/247] Bump airOS to 0.2.7 supporting firmware 8.7.11 (#150298) --- homeassistant/components/airos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index b9bd2db1ae478..84003c19b8982 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.2.6"] + "requirements": ["airos==0.2.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index bae123d243935..3fa122c88c6c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.6 +airos==0.2.7 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b83a0c44d520..b27e0111a0ea0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.6 +airos==0.2.7 # homeassistant.components.airthings_ble airthings-ble==0.9.2 From 0c74e22069bb39fa4a10c7fab85bda00f56f12c2 Mon Sep 17 00:00:00 2001 From: Philipp Waller <1090452+philippwaller@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:14:31 +0200 Subject: [PATCH 155/247] Update knx-frontend to 2025.8.9.63154 (#150323) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f3013de45564c..312ea56972f38 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.8.0", "xknxproject==3.8.2", - "knx-frontend==2025.8.6.52906" + "knx-frontend==2025.8.9.63154" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 3fa122c88c6c4..5350006edfe19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ kiwiki-client==0.1.1 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.8.6.52906 +knx-frontend==2025.8.9.63154 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b27e0111a0ea0..3ec044ca49438 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1129,7 +1129,7 @@ kegtron-ble==0.4.0 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.8.6.52906 +knx-frontend==2025.8.9.63154 # homeassistant.components.konnected konnected==1.2.0 From 3158aa88914bd90a272d4ff001275d4bd8b6ccc0 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Mon, 11 Aug 2025 09:43:09 +0200 Subject: [PATCH 156/247] Update pystiebeleltron to 0.2.3 (#150339) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/stiebel_eltron/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index f8140ed36d7cf..7418c5b7b3228 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/stiebel_eltron", "iot_class": "local_polling", "loggers": ["pymodbus", "pystiebeleltron"], - "requirements": ["pystiebeleltron==0.1.0"] + "requirements": ["pystiebeleltron==0.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5350006edfe19..7f79ce3c8e7dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2385,7 +2385,7 @@ pyspeex-noise==1.0.2 pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron -pystiebeleltron==0.1.0 +pystiebeleltron==0.2.3 # homeassistant.components.suez_water pysuezV2==2.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ec044ca49438..749344cc2cee1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1988,7 +1988,7 @@ pyspeex-noise==1.0.2 pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron -pystiebeleltron==0.1.0 +pystiebeleltron==0.2.3 # homeassistant.components.suez_water pysuezV2==2.0.7 From 6f5d72fd81a58b113686d2c81ad6db128154177e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 11 Aug 2025 10:58:03 +0200 Subject: [PATCH 157/247] Update frontend to 20250811.0 (#150404) --- 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 61ca88ba70ac0..3488ddc5e5cb1 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==20250806.0"] + "requirements": ["home-assistant-frontend==20250811.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 690722545371c..cf29a29a24619 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==4.0.2 hass-nabucasa==0.111.2 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250806.0 +home-assistant-frontend==20250811.0 home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7f79ce3c8e7dc..a1afc52ba07f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ hole==0.9.0 holidays==0.78 # homeassistant.components.frontend -home-assistant-frontend==20250806.0 +home-assistant-frontend==20250811.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 749344cc2cee1..212217d2a4e5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ hole==0.9.0 holidays==0.78 # homeassistant.components.frontend -home-assistant-frontend==20250806.0 +home-assistant-frontend==20250811.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 From 5fdd04b86057aceda0c560b2662bd4e0b21db7a6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:55:47 +0200 Subject: [PATCH 158/247] Handle empty electricity RAW sensors in Tuya (#150406) --- homeassistant/components/tuya/models.py | 6 ++++-- homeassistant/components/tuya/sensor.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/models.py b/homeassistant/components/tuya/models.py index 43e4c04c5186b..059889b754ffa 100644 --- a/homeassistant/components/tuya/models.py +++ b/homeassistant/components/tuya/models.py @@ -108,7 +108,7 @@ def from_json(cls, data: str) -> Self: raise NotImplementedError("from_json is not implemented for this type") @classmethod - def from_raw(cls, data: str) -> Self: + def from_raw(cls, data: str) -> Self | None: """Decode base64 string and return a ComplexTypeData object.""" raise NotImplementedError("from_raw is not implemented for this type") @@ -127,9 +127,11 @@ def from_json(cls, data: str) -> Self: return cls(**json.loads(data.lower())) @classmethod - def from_raw(cls, data: str) -> Self: + def from_raw(cls, data: str) -> Self | None: """Decode base64 string and return a ElectricityTypeData object.""" raw = base64.b64decode(data) + if len(raw) == 0: + return None voltage = struct.unpack(">H", raw[0:2])[0] / 10.0 electriccurrent = struct.unpack(">L", b"\x00" + raw[2:5])[0] / 1000.0 power = struct.unpack(">L", b"\x00" + raw[5:8])[0] / 1000.0 diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 93b1780aeb9fe..5fa820d0852e2 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1555,10 +1555,11 @@ def native_value(self) -> StateType: if ( self.entity_description.complex_type is None or self.entity_description.subkey is None + or (raw_values := self.entity_description.complex_type.from_raw(value)) + is None ): return None - values = self.entity_description.complex_type.from_raw(value) - return getattr(values, self.entity_description.subkey) + return getattr(raw_values, self.entity_description.subkey) # Valid string or enum value return value From dc5d159ffbb40dc7a9cb42b761fd27f54a16947e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 11 Aug 2025 14:09:04 +0200 Subject: [PATCH 159/247] Lower Z-Wave firmware check delay (#150411) --- homeassistant/components/zwave_js/update.py | 13 ++++----- tests/components/zwave_js/test_discovery.py | 12 ++++++++- tests/components/zwave_js/test_update.py | 30 ++++++++++----------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 42a4b4cf6dd60..869767de3e4b3 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -43,7 +43,7 @@ PARALLEL_UPDATES = 1 UPDATE_DELAY_STRING = "delay" -UPDATE_DELAY_INTERVAL = 5 # In minutes +UPDATE_DELAY_INTERVAL = 15 # In seconds ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware" @@ -130,11 +130,11 @@ async def async_setup_entry( @callback def async_add_firmware_update_entity(node: ZwaveNode) -> None: """Add firmware update entity.""" - # We need to delay the first update of each entity to avoid flooding the network - # so we maintain a counter to schedule first update in UPDATE_DELAY_INTERVAL - # minute increments. + # Delay the first update of each entity to avoid spamming the firmware server. + # Maintain a counter to schedule first update in UPDATE_DELAY_INTERVAL + # second increments. cnt[UPDATE_DELAY_STRING] += 1 - delay = timedelta(minutes=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL)) + delay = timedelta(seconds=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL)) driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. if node.is_controller_node: @@ -429,7 +429,8 @@ async def async_added_to_hass(self) -> None: ): self._attr_latest_version = self._attr_installed_version - # Spread updates out in 5 minute increments to avoid flooding the network + # Spread updates out in 15 second increments + # to avoid spamming the firmware server self.async_on_remove( async_call_later(self.hass, self._delay, self._async_update) ) diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 9109d6a4048a3..6a4752d536b82 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -28,7 +28,13 @@ DynamicCurrentTempClimateDataTemplate, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_UNKNOWN, + EntityCategory, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util @@ -253,6 +259,7 @@ async def test_merten_507801_disabled_enitites( assert updated_entry.disabled is False +@pytest.mark.parametrize("platforms", [[Platform.BUTTON, Platform.NUMBER]]) async def test_zooz_zen72( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -324,6 +331,9 @@ async def test_zooz_zen72( assert args["value"] is True +@pytest.mark.parametrize( + "platforms", [[Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]] +) async def test_indicator_test( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index fbe0a8bbea7c5..13651c204141e 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -167,7 +167,7 @@ async def test_update_entity_states( client.async_send_command.return_value = {"updates": []} - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -186,7 +186,7 @@ async def test_update_entity_states( client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=2)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -224,7 +224,7 @@ async def test_update_entity_states( client.async_send_command.return_value = {"updates": []} - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=3)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=3)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -246,7 +246,7 @@ async def test_update_entity_install_raises( """Test update entity install raises exception.""" client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() # Test failed installation by driver @@ -279,7 +279,7 @@ async def test_update_entity_sleep( client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() # Two nodes in total, the controller node and the zen_31 node. @@ -324,7 +324,7 @@ async def test_update_entity_dead( client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() # Two nodes in total, the controller node and the zen_31 node. @@ -368,14 +368,14 @@ async def test_update_entity_ha_not_running( # Update should be delayed by a day because Home Assistant is not running hass.set_state(CoreState.starting) - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15)) await hass.async_block_till_done() assert client.async_send_command.call_count == 0 hass.set_state(CoreState.running) - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() # Two nodes in total, the controller node and the zen_31 node. @@ -401,7 +401,7 @@ async def test_update_entity_update_failure( assert client.async_send_command.call_count == 0 client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test") - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() entity_ids = (CONTROLLER_UPDATE_ENTITY, NODE_UPDATE_ENTITY) @@ -509,7 +509,7 @@ async def test_update_entity_progress( client.async_send_command.return_value = FIRMWARE_UPDATES driver = client.driver - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -657,7 +657,7 @@ async def test_update_entity_install_failed( driver = client.driver client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -733,7 +733,7 @@ async def test_update_entity_reload( client.async_send_command.return_value = {"updates": []} - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -742,7 +742,7 @@ async def test_update_entity_reload( client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=2)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -774,7 +774,7 @@ async def test_update_entity_reload( await hass.async_block_till_done() # Trigger another update and make sure the skipped version is still skipped - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=4)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=4)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -809,7 +809,7 @@ async def test_update_entity_delay( assert client.async_send_command.call_count == 0 - update_interval = timedelta(minutes=5) + update_interval = timedelta(seconds=15) freezer.tick(update_interval) async_fire_time_changed(hass) await hass.async_block_till_done() From 7ed14f0afdceddb648b3ffb89fde34f818ebe86c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Aug 2025 12:15:36 +0000 Subject: [PATCH 160/247] Bump version to 2025.8.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 c4033ac039c4e..c02668d6899d5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -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, 2) diff --git a/pyproject.toml b/pyproject.toml index fb39802e5f425..e869bd0d0cbf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0" +version = "2025.8.1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 1aeced0fe6bf3f775baa5b5f609ee3863e0bd874 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Aug 2025 20:25:24 +0200 Subject: [PATCH 161/247] Fix issue with Tuya suggested unit (#150414) --- homeassistant/components/tuya/sensor.py | 2 + tests/components/tuya/__init__.py | 1 + .../tuya/snapshots/test_sensor.ambr | 52 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 5fa820d0852e2..a4dd8a0189c9e 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1496,6 +1496,7 @@ def __init__( self.unique_id, ) self._attr_device_class = None + self._attr_suggested_unit_of_measurement = None return uoms = DEVICE_CLASS_UNITS[self.device_class] @@ -1506,6 +1507,7 @@ def __init__( # Unknown unit of measurement, device class should not be used. if uom is None: self._attr_device_class = None + self._attr_suggested_unit_of_measurement = None return # Found unit of measurement, use the standardized Unit diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index a8182adb90c89..e5f777a88aebb 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -114,6 +114,7 @@ "kj_CAjWAxBUZt7QZHfz": [ # https://github.com/home-assistant/core/issues/146023 Platform.FAN, + Platform.SENSOR, Platform.SWITCH, ], "kj_yrzylxax1qspdgpp": [ diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 882839a666506..ea9bd75ed2e5b 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -1649,6 +1649,58 @@ 'state': '220.4', }) # --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][sensor.hl400_pm2_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hl400_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pm25', + 'unique_id': 'tuya.152027113c6105cce49cpm25', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][sensor.hl400_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 PM2.5', + 'state_class': , + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'sensor.hl400_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.0', + }) +# --- # name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 1e87f0cab10c5727c4d0552bb27d323909f655ee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Aug 2025 18:29:46 +0000 Subject: [PATCH 162/247] Revert "Update pystiebeleltron to 0.2.3 (#150339)" This reverts commit 3158aa88914bd90a272d4ff001275d4bd8b6ccc0. --- homeassistant/components/stiebel_eltron/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index 7418c5b7b3228..f8140ed36d7cf 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/stiebel_eltron", "iot_class": "local_polling", "loggers": ["pymodbus", "pystiebeleltron"], - "requirements": ["pystiebeleltron==0.2.3"] + "requirements": ["pystiebeleltron==0.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index a1afc52ba07f0..65f10e07778a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2385,7 +2385,7 @@ pyspeex-noise==1.0.2 pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron -pystiebeleltron==0.2.3 +pystiebeleltron==0.1.0 # homeassistant.components.suez_water pysuezV2==2.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 212217d2a4e5b..f92a78bf9d336 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1988,7 +1988,7 @@ pyspeex-noise==1.0.2 pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron -pystiebeleltron==0.2.3 +pystiebeleltron==0.1.0 # homeassistant.components.suez_water pysuezV2==2.0.7 From 8d49cb1195d9b3a0f1600cab2afacb4e66921d68 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:57:11 +0200 Subject: [PATCH 163/247] Add pymodbus to package constraints (#150420) --- 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 cf29a29a24619..601c0cf323864 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -216,3 +216,8 @@ rpds-py==0.24.0 # Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI num2words==0.5.14 + +# pymodbus does not follow SemVer, and it keeps getting +# downgraded or upgraded by custom components +# This ensures all use the same version +pymodbus==3.9.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 779393d2c79a6..eb986fd8bb0ab 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -242,6 +242,11 @@ # Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI num2words==0.5.14 + +# pymodbus does not follow SemVer, and it keeps getting +# downgraded or upgraded by custom components +# This ensures all use the same version +pymodbus==3.9.2 """ GENERATED_MESSAGE = ( From 391c9a679ec1a0ee3580f05fd1b72aed3984e184 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:55:43 +0200 Subject: [PATCH 164/247] Fix enphase_envoy non existing via device warning at first config. (#149010) Co-authored-by: Joost Lekkerkerker --- .../components/enphase_envoy/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index e95ab1179e109..62d276b42242e 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pyenphase import Envoy from homeassistant.const import CONF_HOST @@ -42,6 +44,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b }, ) + # register envoy before via_device is used + device_registry = dr.async_get(hass) + if TYPE_CHECKING: + assert envoy.serial_number + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, envoy.serial_number)}, + manufacturer="Enphase", + name=coordinator.name, + model=envoy.envoy_model, + sw_version=str(envoy.firmware), + hw_version=envoy.part_number, + serial_number=envoy.serial_number, + ) + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) From b5bd61b20a1f9346f751a71bb1bb3f86b151efc2 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 11 Aug 2025 11:47:29 -0500 Subject: [PATCH 165/247] Handle non-streaming TTS case correctly (#150218) --- homeassistant/components/tts/__init__.py | 7 +- homeassistant/components/tts/entity.py | 12 +++ tests/components/tts/test_entity.py | 28 +++++++ tests/components/tts/test_init.py | 31 +++++++ .../wyoming/snapshots/test_tts.ambr | 80 ------------------- 5 files changed, 77 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index cf9099448df31..629332d9d646d 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -976,11 +976,15 @@ async def _async_generate_tts_audio( if engine_instance.name is None or engine_instance.name is UNDEFINED: raise HomeAssistantError("TTS engine name is not set.") - if isinstance(engine_instance, Provider): + if isinstance(engine_instance, Provider) or ( + not engine_instance.async_supports_streaming_input() + ): + # Non-streaming if isinstance(message_or_stream, str): message = message_or_stream else: message = "".join([chunk async for chunk in message_or_stream]) + extension, data = await engine_instance.async_internal_get_tts_audio( message, language, options ) @@ -996,6 +1000,7 @@ async def make_data_generator(data: bytes) -> AsyncGenerator[bytes]: data_gen = make_data_generator(data) else: + # Streaming if isinstance(message_or_stream, str): async def gen_stream() -> AsyncGenerator[str]: diff --git a/homeassistant/components/tts/entity.py b/homeassistant/components/tts/entity.py index aea5be6d0da95..77abaa26baba5 100644 --- a/homeassistant/components/tts/entity.py +++ b/homeassistant/components/tts/entity.py @@ -191,6 +191,18 @@ def get_tts_audio( """Load tts audio file from the engine.""" raise NotImplementedError + @final + async def async_internal_get_tts_audio( + self, message: str, language: str, options: dict[str, Any] + ) -> TtsAudioType: + """Load tts audio file from the engine and update state. + + Return a tuple of file extension and data as bytes. + """ + self.__last_tts_loaded = dt_util.utcnow().isoformat() + self.async_write_ha_state() + return await self.async_get_tts_audio(message, language, options=options) + async def async_get_tts_audio( self, message: str, language: str, options: dict[str, Any] ) -> TtsAudioType: diff --git a/tests/components/tts/test_entity.py b/tests/components/tts/test_entity.py index 8648ca95e932f..308d3bb0fca67 100644 --- a/tests/components/tts/test_entity.py +++ b/tests/components/tts/test_entity.py @@ -175,3 +175,31 @@ def get_tts_audio( sync_non_streaming_entity = SyncNonStreamingEntity() assert sync_non_streaming_entity.async_supports_streaming_input() is False + + +async def test_internal_get_tts_audio_writes_state( + hass: HomeAssistant, + mock_tts_entity: MockTTSEntity, +) -> None: + """Test that only async_internal_get_tts_audio updates and writes the state.""" + + entity_id = f"{tts.DOMAIN}.{TEST_DOMAIN}" + + config_entry = await mock_config_entry_setup(hass, mock_tts_entity) + assert config_entry.state is ConfigEntryState.LOADED + state1 = hass.states.get(entity_id) + assert state1 is not None + + # State should *not* change with external method + await mock_tts_entity.async_get_tts_audio("test message", hass.config.language, {}) + state2 = hass.states.get(entity_id) + assert state2 is not None + assert state1.state == state2.state + + # State *should* change with internal method + await mock_tts_entity.async_internal_get_tts_audio( + "test message", hass.config.language, {} + ) + state3 = hass.states.get(entity_id) + assert state3 is not None + assert state1.state != state3.state diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index db42da5de0eec..be155aae182b9 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -2032,3 +2032,34 @@ async def consume_cache(cache: tts.TTSCache): assert await consume_mid_data_task == b"012" with pytest.raises(ValueError): assert await consume_pre_data_loaded_task == b"012" + + +async def test_async_internal_get_tts_audio_called( + hass: HomeAssistant, + mock_tts_entity: MockTTSEntity, + hass_client: ClientSessionGenerator, +) -> None: + """Test that non-streaming entity has its async_internal_get_tts_audio method called.""" + + await mock_config_entry_setup(hass, mock_tts_entity) + + # Non-streaming + assert mock_tts_entity.async_supports_streaming_input() is False + + with patch( + "homeassistant.components.tts.entity.TextToSpeechEntity.async_internal_get_tts_audio" + ) as internal_get_tts_audio: + media_source_id = tts.generate_media_source_id( + hass, + "test message", + "tts.test", + "en_US", + cache=None, + ) + + url = await get_media_source_url(hass, media_source_id) + client = await hass_client() + await client.get(url) + + # async_internal_get_tts_audio is called + internal_get_tts_audio.assert_called_once_with("test message", "en_US", {}) diff --git a/tests/components/wyoming/snapshots/test_tts.ambr b/tests/components/wyoming/snapshots/test_tts.ambr index 67c9b24160c72..53cc02eaacf2f 100644 --- a/tests/components/wyoming/snapshots/test_tts.ambr +++ b/tests/components/wyoming/snapshots/test_tts.ambr @@ -1,19 +1,6 @@ # serializer version: 1 # name: test_get_tts_audio list([ - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-start', - }), - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize-chunk', - }), dict({ 'data': dict({ 'text': 'Hello world', @@ -21,29 +8,10 @@ 'payload': None, 'type': 'synthesize', }), - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-stop', - }), ]) # --- # name: test_get_tts_audio_different_formats list([ - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-start', - }), - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize-chunk', - }), dict({ 'data': dict({ 'text': 'Hello world', @@ -51,29 +19,10 @@ 'payload': None, 'type': 'synthesize', }), - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-stop', - }), ]) # --- # name: test_get_tts_audio_different_formats.1 list([ - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-start', - }), - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize-chunk', - }), dict({ 'data': dict({ 'text': 'Hello world', @@ -81,12 +30,6 @@ 'payload': None, 'type': 'synthesize', }), - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-stop', - }), ]) # --- # name: test_get_tts_audio_streaming @@ -128,23 +71,6 @@ # --- # name: test_voice_speaker list([ - dict({ - 'data': dict({ - 'voice': dict({ - 'name': 'voice1', - 'speaker': 'speaker1', - }), - }), - 'payload': None, - 'type': 'synthesize-start', - }), - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize-chunk', - }), dict({ 'data': dict({ 'text': 'Hello world', @@ -156,11 +82,5 @@ 'payload': None, 'type': 'synthesize', }), - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-stop', - }), ]) # --- From e22e7f1bcf0c518419c67c3f2c11897bd2f60b25 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Wed, 13 Aug 2025 19:53:21 +0100 Subject: [PATCH 166/247] Pi_hole - Account for auth succeeding when it shouldn't (#150413) --- homeassistant/components/pi_hole/__init__.py | 7 +++++++ tests/components/pi_hole/__init__.py | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index f73b7156d3ea1..ae51fe166c4f6 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -217,6 +217,13 @@ async def determine_api_version( _LOGGER.debug( "Connection to %s failed: %s, trying API version 5", holeV6.base_url, ex_v6 ) + else: + # It seems that occasionally the auth can succeed unexpectedly when there is a valid session + _LOGGER.warning( + "Authenticated with %s through v6 API, but succeeded with an incorrect password. This is a known bug", + holeV6.base_url, + ) + return 6 holeV5 = api_by_version(hass, entry, 5, password="wrong_token") try: await holeV5.get_data() diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index c20f22ac58d2e..c2edb51e06641 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -221,12 +221,16 @@ async def authenticate_side_effect(*_args, **_kwargs): if wrong_host: raise HoleConnectionError("Cannot authenticate with Pi-hole: err") password = getattr(mocked_hole, "password", None) + if ( raise_exception or incorrect_app_password + or api_version == 5 or (api_version == 6 and password not in ["newkey", "apikey"]) ): - if api_version == 6: + if api_version == 6 and ( + incorrect_app_password or password not in ["newkey", "apikey"] + ): raise HoleError("Authentication failed: Invalid password") raise HoleConnectionError From 8b2fce9c339449f07b6a133fde8b06f0a93906e6 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:51:22 +0200 Subject: [PATCH 167/247] Bump habiticalib to version 0.4.2 (#150417) --- homeassistant/components/habitica/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index d890ed2367674..e0c58383bcc9b 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["habiticalib"], "quality_scale": "platinum", - "requirements": ["habiticalib==0.4.1"] + "requirements": ["habiticalib==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 65f10e07778a4..89373b70a0760 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,7 +1127,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.1 +habiticalib==0.4.2 # homeassistant.components.bluetooth habluetooth==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f92a78bf9d336..0b4c4cd0ce5c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.1 +habiticalib==0.4.2 # homeassistant.components.bluetooth habluetooth==4.0.2 From 2ad470d1729e4ef09c864e02f4b04638aab87653 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Fri, 15 Aug 2025 07:53:48 -0400 Subject: [PATCH 168/247] Fix optimistic set to false for template entities (#150421) --- homeassistant/components/template/entity.py | 10 ++-- .../components/template/template_entity.py | 2 +- .../template/test_alarm_control_panel.py | 32 +++++++++++++ tests/components/template/test_cover.py | 29 ++++++++++- tests/components/template/test_fan.py | 33 +++++++++++++ tests/components/template/test_light.py | 36 ++++++++++++++ tests/components/template/test_lock.py | 33 +++++++++++++ tests/components/template/test_number.py | 31 ++++++++++++ tests/components/template/test_select.py | 36 ++++++++++++++ tests/components/template/test_switch.py | 37 ++++++++++++++ tests/components/template/test_vacuum.py | 48 +++++++++++++++++++ 11 files changed, 322 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/template/entity.py b/homeassistant/components/template/entity.py index 03a93f50ec3c8..4901a7a7be8fe 100644 --- a/homeassistant/components/template/entity.py +++ b/homeassistant/components/template/entity.py @@ -34,16 +34,20 @@ def __init__( self._action_scripts: dict[str, Script] = {} if self._optimistic_entity: + optimistic = config.get(CONF_OPTIMISTIC) + self._template = config.get(CONF_STATE) - optimistic = self._template is None + assumed_optimistic = self._template is None if self._extra_optimistic_options: - optimistic = optimistic and all( + assumed_optimistic = assumed_optimistic and all( config.get(option) is None for option in self._extra_optimistic_options ) - self._attr_assumed_state = optimistic or config.get(CONF_OPTIMISTIC, False) + self._attr_assumed_state = optimistic or ( + optimistic is None and assumed_optimistic + ) if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 1bc49bceafd2a..3ba89cae1f4b5 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -102,7 +102,7 @@ TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA = { - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_OPTIMISTIC): cv.boolean, } diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index c1df654e3280d..319d02a1056ae 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -973,3 +973,35 @@ async def test_optimistic(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == AlarmControlPanelState.ARMED_HOME + + +@pytest.mark.parametrize( + ("count", "panel_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ states('alarm_control_panel.test') }}", + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_panel") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_arm_away", + {"entity_id": TEST_ENTITY_ID, "code": "1234"}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 692567c7aa897..2a83967b048c7 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -628,11 +628,38 @@ async def test_template_position( ], ) @pytest.mark.usefixtures("setup_cover") -async def test_template_not_optimistic(hass: HomeAssistant) -> None: +async def test_template_not_optimistic( + hass: HomeAssistant, + calls: list[ServiceCall], +) -> None: """Test the is_closed attribute.""" state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN + # Test to make sure optimistic is not set with only a position template. + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + # Test to make sure optimistic is not set with only a position template. + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + @pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")]) @pytest.mark.parametrize( diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index b9161edf61aea..81486d75137d3 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -1885,6 +1885,39 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: assert state.state == STATE_OFF +@pytest.mark.parametrize( + ("count", "fan_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ is_state('sensor.test_sensor', 'on') }}", + "turn_on": [], + "turn_off": [], + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_fan") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + fan.DOMAIN, + "turn_on", + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 0549f9981e7e4..e5d05cfa08faa 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -2795,6 +2795,42 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: assert state.state == STATE_OFF +@pytest.mark.parametrize( + ("count", "light_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ is_state('light.test_state', 'on') }}", + "turn_on": [], + "turn_off": [], + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + ("style", "expected"), + [ + (ConfigurationStyle.MODERN, STATE_OFF), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.usefixtures("setup_light") +async def test_not_optimistic(hass: HomeAssistant, expected: str) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + light.DOMAIN, + "turn_on", + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 823306015bfe4..6a4164fb80272 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -1190,6 +1190,39 @@ async def test_optimistic(hass: HomeAssistant) -> None: assert state.state == LockState.UNLOCKED +@pytest.mark.parametrize( + ("count", "lock_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ is_state('sensor.test_state', 'on') }}", + "lock": [], + "unlock": [], + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_lock") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_LOCK, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == LockState.UNLOCKED + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index 0ae98a23ae45d..f10664e0d5f7f 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -605,6 +605,37 @@ async def test_optimistic(hass: HomeAssistant) -> None: assert float(state.state) == 2 +@pytest.mark.parametrize( + ("count", "number_config"), + [ + ( + 1, + { + "state": "{{ states('sensor.test_state') }}", + "optimistic": False, + "set_value": [], + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_number") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + 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 state.state == STATE_UNKNOWN + + @pytest.mark.parametrize( ("count", "number_config"), [ diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index f613fa865a612..eda27f1810008 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -601,6 +601,42 @@ async def test_optimistic(hass: HomeAssistant) -> None: assert state.state == "yes" +@pytest.mark.parametrize( + ("count", "select_config"), + [ + ( + 1, + { + "state": "{{ states('select.test_state') }}", + "optimistic": False, + "options": "{{ ['test', 'yes', 'no'] }}", + "select_option": [], + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_select") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + # Ensure Trigger template entities update the options list + hass.states.async_set(TEST_STATE_ENTITY_ID, "anything") + await hass.async_block_till_done() + + await hass.services.async_call( + select.DOMAIN, + select.SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: _TEST_SELECT, "option": "test"}, + blocking=True, + ) + + state = hass.states.get(_TEST_SELECT) + assert state.state == STATE_UNKNOWN + + @pytest.mark.parametrize( ("count", "select_config"), [ diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index a32f1df4c76a9..5a884160fe880 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -14,6 +14,7 @@ STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -1267,3 +1268,39 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF + + +@pytest.mark.parametrize( + ("count", "switch_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ is_state('switch.test_state', 'on') }}", + "turn_on": [], + "turn_off": [], + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + ("style", "expected"), + [ + (ConfigurationStyle.MODERN, STATE_OFF), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.usefixtures("setup_switch") +async def test_not_optimistic(hass: HomeAssistant, expected: str) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + switch.DOMAIN, + "turn_on", + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 8c2773956b294..21592718551d4 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -1299,6 +1299,54 @@ async def test_optimistic_option( assert state.state == VacuumActivity.DOCKED +@pytest.mark.parametrize( + ("count", "vacuum_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ states('sensor.test_state') }}", + "start": [], + **TEMPLATE_VACUUM_ACTIONS, + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.parametrize( + "service", + [ + vacuum.SERVICE_START, + vacuum.SERVICE_PAUSE, + vacuum.SERVICE_STOP, + vacuum.SERVICE_RETURN_TO_BASE, + vacuum.SERVICE_CLEAN_SPOT, + ], +) +@pytest.mark.usefixtures("setup_vacuum") +async def test_not_optimistic( + hass: HomeAssistant, + service: str, + calls: list[ServiceCall], +) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + vacuum.DOMAIN, + service, + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, From ffbb7a2ab4e9314b854e861dbbf50046c2216938 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 12 Aug 2025 12:45:21 +0200 Subject: [PATCH 169/247] Fix error of the Powerfox integration in combination with the new Powerfox FLOW adapter (#150429) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/powerfox/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/powerfox/__init__.py b/homeassistant/components/powerfox/__init__.py index 8e51985211d42..c2f6830692cc0 100644 --- a/homeassistant/components/powerfox/__init__.py +++ b/homeassistant/components/powerfox/__init__.py @@ -4,7 +4,7 @@ import asyncio -from powerfox import Powerfox, PowerfoxConnectionError +from powerfox import DeviceType, Powerfox, PowerfoxConnectionError from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant @@ -31,7 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) -> raise ConfigEntryNotReady from err coordinators: list[PowerfoxDataUpdateCoordinator] = [ - PowerfoxDataUpdateCoordinator(hass, entry, client, device) for device in devices + PowerfoxDataUpdateCoordinator(hass, entry, client, device) + for device in devices + # Filter out gas meter devices (Powerfox FLOW adapters) as they are not yet supported and cause integration failures + if device.type != DeviceType.GAS_METER ] await asyncio.gather( From c58a1881798d90c07ae02d9f34db8499315f0cc6 Mon Sep 17 00:00:00 2001 From: Kevin David Date: Mon, 11 Aug 2025 16:25:41 -0400 Subject: [PATCH 170/247] Bump python-snoo to 0.7.0 (#150434) --- homeassistant/components/snoo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snoo/manifest.json b/homeassistant/components/snoo/manifest.json index 2afec990e4b20..b47947ab0e019 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.6"] + "requirements": ["python-snoo==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 89373b70a0760..3ebf11e48040d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2512,7 +2512,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.6.6 +python-snoo==0.7.0 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b4c4cd0ce5c0..218ccd65cb230 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2082,7 +2082,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.6.6 +python-snoo==0.7.0 # homeassistant.components.songpal python-songpal==0.16.2 From bd1b81493c69fda5279c27666987bf10d88cf8cd Mon Sep 17 00:00:00 2001 From: wedsa5 Date: Tue, 12 Aug 2025 08:36:52 -0600 Subject: [PATCH 171/247] Fix brightness command not sent when in white color mode (#150439) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/tuya/light.py | 7 +++- tests/components/tuya/test_light.py | 55 +++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 7b73e8259004e..1dc061520e397 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -665,8 +665,11 @@ def turn_on(self, **kwargs: Any) -> None: }, ] - elif ATTR_BRIGHTNESS in kwargs and self._brightness: - brightness = kwargs[ATTR_BRIGHTNESS] + elif self._brightness and (ATTR_BRIGHTNESS in kwargs or ATTR_WHITE in kwargs): + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + else: + brightness = kwargs[ATTR_WHITE] # If there is a min/max value, the brightness is actually limited. # Meaning it is actually not on a 0-255 scale. diff --git a/tests/components/tuya/test_light.py b/tests/components/tuya/test_light.py index e35866138763e..1c6b1138e4ced 100644 --- a/tests/components/tuya/test_light.py +++ b/tests/components/tuya/test_light.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Any from unittest.mock import patch import pytest @@ -66,11 +67,58 @@ async def test_platform_setup_no_discovery( "mock_device_code", ["dj_mki13ie507rlry4r"], ) +@pytest.mark.parametrize( + ("turn_on_input", "expected_commands"), + [ + ( + { + "white": True, + }, + [ + {"code": "switch_led", "value": True}, + {"code": "work_mode", "value": "white"}, + {"code": "bright_value_v2", "value": 546}, + ], + ), + ( + { + "brightness": 150, + }, + [ + {"code": "switch_led", "value": True}, + {"code": "bright_value_v2", "value": 592}, + ], + ), + ( + { + "white": True, + "brightness": 150, + }, + [ + {"code": "switch_led", "value": True}, + {"code": "work_mode", "value": "white"}, + {"code": "bright_value_v2", "value": 592}, + ], + ), + ( + { + "white": 150, + }, + [ + {"code": "switch_led", "value": True}, + {"code": "work_mode", "value": "white"}, + {"code": "bright_value_v2", "value": 592}, + ], + ), + ], +) async def test_turn_on_white( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, mock_device: CustomerDevice, + turn_on_input: dict[str, Any], + expected_commands: list[dict[str, Any]], ) -> None: """Test turn_on service.""" entity_id = "light.garage_light" @@ -83,16 +131,13 @@ async def test_turn_on_white( SERVICE_TURN_ON, { "entity_id": entity_id, - "white": 150, + **turn_on_input, }, ) await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, - [ - {"code": "switch_led", "value": True}, - {"code": "work_mode", "value": "white"}, - ], + expected_commands, ) From 2725abf032a7103277756538c22c6ebe8e98e9cc Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Tue, 12 Aug 2025 09:51:39 +0200 Subject: [PATCH 172/247] Bump cookidoo-api to 0.14.0 (#150450) --- homeassistant/components/cookidoo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cookidoo/manifest.json b/homeassistant/components/cookidoo/manifest.json index 5264e47a70993..b4cf653f810f8 100644 --- a/homeassistant/components/cookidoo/manifest.json +++ b/homeassistant/components/cookidoo/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["cookidoo_api"], "quality_scale": "silver", - "requirements": ["cookidoo-api==0.12.2"] + "requirements": ["cookidoo-api==0.14.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ebf11e48040d..b9a87f17c338d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -743,7 +743,7 @@ connect-box==0.3.1 construct==2.10.68 # homeassistant.components.cookidoo -cookidoo-api==0.12.2 +cookidoo-api==0.14.0 # homeassistant.components.backup # homeassistant.components.utility_meter diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 218ccd65cb230..3b0c118db552e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -646,7 +646,7 @@ colorthief==0.2.1 construct==2.10.68 # homeassistant.components.cookidoo -cookidoo-api==0.12.2 +cookidoo-api==0.14.0 # homeassistant.components.backup # homeassistant.components.utility_meter From fed6f19edfdbe12145e6ee142f4da9292297022c Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 12 Aug 2025 16:42:40 +0800 Subject: [PATCH 173/247] Fix YoLink valve state when device running in class A mode (#150456) --- homeassistant/components/yolink/strings.json | 3 +++ homeassistant/components/yolink/valve.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 0eb9de974693e..4215031d90497 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -47,6 +47,9 @@ "exceptions": { "invalid_config_entry": { "message": "Config entry not found or not loaded!" + }, + "valve_inoperable_currently": { + "message": "The Valve cannot be operated currently." } }, "entity": { diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py index 06dee8af5404f..e63488194d08a 100644 --- a/homeassistant/components/yolink/valve.py +++ b/homeassistant/components/yolink/valve.py @@ -21,6 +21,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DEV_MODEL_WATER_METER_YS5007, DOMAIN @@ -130,6 +131,13 @@ def update_entity_state(self, state: dict[str, str | list[str]]) -> None: async def _async_invoke_device(self, state: str) -> None: """Call setState api to change valve state.""" + if ( + self.coordinator.device.is_support_mode_switching() + and self.coordinator.dev_net_type == ATTR_DEVICE_MODEL_A + ): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="valve_inoperable_currently" + ) if ( self.coordinator.device.device_type == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER @@ -155,10 +163,4 @@ async def async_close_valve(self) -> None: @property def available(self) -> bool: """Return true is device is available.""" - if ( - self.coordinator.device.is_support_mode_switching() - and self.coordinator.dev_net_type is not None - ): - # When the device operates in Class A mode, it cannot be controlled. - return self.coordinator.dev_net_type != ATTR_DEVICE_MODEL_A return super().available From 3d4d57fa3224d82ce8a1557a7714ab69a93b5c54 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:55:21 +0100 Subject: [PATCH 174/247] Additional Fix error on startup when no Apps or Radio plugins are installed for Squeezebox (#150475) --- homeassistant/components/squeezebox/browse_media.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index e14f1989cbe18..cebd4fcb04f9f 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -157,7 +157,7 @@ async def async_init(self, player: Player, browse_limit: int) -> None: cmd = ["apps", 0, browse_limit] result = await player.async_query(*cmd) - if result["appss_loop"]: + if result and result.get("appss_loop"): for app in result["appss_loop"]: app_cmd = "app-" + app["cmd"] if app_cmd not in self.known_apps_radios: @@ -169,7 +169,7 @@ async def async_init(self, player: Player, browse_limit: int) -> None: ) cmd = ["radios", 0, browse_limit] result = await player.async_query(*cmd) - if result["radioss_loop"]: + if result and result.get("radioss_loop"): for app in result["radioss_loop"]: app_cmd = "app-" + app["cmd"] if app_cmd not in self.known_apps_radios: From b0ab3cddb844ee3ad30f69feb52944fb81241d9b Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:58:03 +0200 Subject: [PATCH 175/247] Fix re-auth flow for Volvo integration (#150478) --- homeassistant/components/volvo/config_flow.py | 6 +-- tests/components/volvo/test_config_flow.py | 49 ++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/volvo/config_flow.py b/homeassistant/components/volvo/config_flow.py index f187d751a2dce..0ae0e54077e78 100644 --- a/homeassistant/components/volvo/config_flow.py +++ b/homeassistant/components/volvo/config_flow.py @@ -69,7 +69,7 @@ def logger(self) -> logging.Logger: async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create an entry for the flow.""" - self._config_data |= data + self._config_data |= (self.init_data or {}) | data return await self.async_step_api_key() async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult: @@ -77,7 +77,7 @@ async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult: return await self.async_step_reauth_confirm() async def async_step_reconfigure( - self, _: dict[str, Any] | None = None + self, data: dict[str, Any] | None = None ) -> ConfigFlowResult: """Reconfigure the entry.""" return await self.async_step_api_key() @@ -121,7 +121,7 @@ async def async_step_api_key( if user_input is None: if self.source == SOURCE_REAUTH: - user_input = self._config_data = dict(self._get_reauth_entry().data) + user_input = self._config_data api = _create_volvo_cars_api( self.hass, self._config_data[CONF_TOKEN][CONF_ACCESS_TOKEN], diff --git a/tests/components/volvo/test_config_flow.py b/tests/components/volvo/test_config_flow.py index 91a7803dce57e..3129b1383fea0 100644 --- a/tests/components/volvo/test_config_flow.py +++ b/tests/components/volvo/test_config_flow.py @@ -13,7 +13,7 @@ from homeassistant import config_entries from homeassistant.components.volvo.const import CONF_VIN, DOMAIN from homeassistant.config_entries import ConfigFlowResult -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -117,6 +117,53 @@ async def test_reauth_flow( assert result["reason"] == "reauth_successful" +@pytest.mark.usefixtures("current_request_with_host") +async def test_reauth_no_stale_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + mock_config_flow_api: VolvoCarsApi, +) -> None: + """Test if reauthentication flow does not use stale data.""" + old_access_token = mock_config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN] + + with patch( + "homeassistant.components.volvo.config_flow._create_volvo_cars_api", + return_value=mock_config_flow_api, + ) as mock_create_volvo_cars_api: + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URI, + }, + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + result = await _async_run_flow_to_completion( + hass, + result, + mock_config_flow_api, + has_vin_step=False, + is_reauth=True, + ) + + assert mock_create_volvo_cars_api.called + call = mock_create_volvo_cars_api.call_args_list[0] + access_token_arg = call.args[1] + assert old_access_token != access_token_arg + + async def test_reconfigure_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From 8f94657b0c65cbaf72dcb22bb44242a808a4c8b3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 12 Aug 2025 13:47:11 +0200 Subject: [PATCH 176/247] Improve Z-Wave manual config flow step description (#150479) --- .../components/zwave_js/config_flow.py | 28 +++++++++++++++++-- .../components/zwave_js/strings.json | 8 ++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 6121bd0050891..b72a71279ab49 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -88,11 +88,16 @@ CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY, } +EXAMPLE_SERVER_URL = "ws://localhost:3000" ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61") NETWORK_TYPE_NEW = "new" NETWORK_TYPE_EXISTING = "existing" +ZWAVE_JS_SERVER_INSTRUCTIONS = ( + "https://www.home-assistant.io/integrations/zwave_js/" + "#advanced-installation-instructions" +) ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = ( "https://www.home-assistant.io/integrations/zwave_js/" "#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui" @@ -529,7 +534,12 @@ async def async_step_manual( """Handle a manual configuration.""" if user_input is None: return self.async_show_form( - step_id="manual", data_schema=get_manual_schema({}) + step_id="manual", + data_schema=get_manual_schema({}), + description_placeholders={ + "example_server_url": EXAMPLE_SERVER_URL, + "server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS, + }, ) errors = {} @@ -558,7 +568,13 @@ async def async_step_manual( return self._async_create_entry_from_vars() return self.async_show_form( - step_id="manual", data_schema=get_manual_schema(user_input), errors=errors + step_id="manual", + data_schema=get_manual_schema(user_input), + description_placeholders={ + "example_server_url": EXAMPLE_SERVER_URL, + "server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS, + }, + errors=errors, ) async def async_step_hassio( @@ -1016,6 +1032,10 @@ async def async_step_manual_reconfigure( return self.async_show_form( step_id="manual_reconfigure", data_schema=get_manual_schema({CONF_URL: config_entry.data[CONF_URL]}), + description_placeholders={ + "example_server_url": EXAMPLE_SERVER_URL, + "server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS, + }, ) errors = {} @@ -1046,6 +1066,10 @@ async def async_step_manual_reconfigure( return self.async_show_form( step_id="manual_reconfigure", data_schema=get_manual_schema(user_input), + description_placeholders={ + "example_server_url": EXAMPLE_SERVER_URL, + "server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS, + }, errors=errors, ) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 8ac356a40b043..0ff635578ea32 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -82,13 +82,21 @@ "title": "Installing add-on" }, "manual": { + "description": "The Z-Wave integration requires a running Z-Wave Server. If you don't already have that set up, please read the [instructions]({server_instructions}) in our documentation.\n\nWhen you have a Z-Wave Server running, enter its URL below to allow the integration to connect.", "data": { "url": "[%key:common::config_flow::data::url%]" + }, + "data_description": { + "url": "The URL of the Z-Wave Server WebSocket API, e.g. {example_server_url}" } }, "manual_reconfigure": { + "description": "[%key:component::zwave_js::config::step::manual::description%]", "data": { "url": "[%key:common::config_flow::data::url%]" + }, + "data_description": { + "url": "[%key:component::zwave_js::config::step::manual::data_description::url%]" } }, "on_supervisor": { From d9ebda49104c1ceb3f20beb3bb5eff7c3b983e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Tue, 12 Aug 2025 13:58:38 +0200 Subject: [PATCH 177/247] Add missing boost2 code for Miele hobs (#150481) --- homeassistant/components/miele/const.py | 1 + homeassistant/components/miele/icons.json | 3 +- homeassistant/components/miele/strings.json | 3 +- .../miele/snapshots/test_sensor.ambr | 28 +++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index e8b626af785a8..3b5b13398a58a 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -1330,4 +1330,5 @@ class PlatePowerStep(MieleEnum): plate_step_17 = 17 plate_step_18 = 18 plate_step_boost = 117, 118, 218 + plate_step_boost_2 = 217 missing2none = -9999 diff --git a/homeassistant/components/miele/icons.json b/homeassistant/components/miele/icons.json index 77d94c49ffa68..a5dbeb4ec2dd9 100644 --- a/homeassistant/components/miele/icons.json +++ b/homeassistant/components/miele/icons.json @@ -76,7 +76,8 @@ "plate_step_16": "mdi:circle-slice-7", "plate_step_17": "mdi:circle-slice-8", "plate_step_18": "mdi:circle-slice-8", - "plate_step_boost": "mdi:alpha-b-circle-outline" + "plate_step_boost": "mdi:alpha-b-circle-outline", + "plate_step_boost_2": "mdi:alpha-b-circle" } }, "program_type": { diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 90689a3d9cc1b..cb9861e0246ce 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -223,7 +223,8 @@ "plate_step_16": "8\u2022", "plate_step_17": "9", "plate_step_18": "9\u2022", - "plate_step_boost": "Boost" + "plate_step_boost": "Boost", + "plate_step_boost_2": "Boost 2" } }, "drying_step": { diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 2805a6830773d..5d941550f419b 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -208,6 +208,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -266,6 +267,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -304,6 +306,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -362,6 +365,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -400,6 +404,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -458,6 +463,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -496,6 +502,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -554,6 +561,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -592,6 +600,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -650,6 +659,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -688,6 +698,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -746,6 +757,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -784,6 +796,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -842,6 +855,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -880,6 +894,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -938,6 +953,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -976,6 +992,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1034,6 +1051,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1457,6 +1475,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1515,6 +1534,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1553,6 +1573,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1611,6 +1632,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1649,6 +1671,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1707,6 +1730,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1745,6 +1769,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1803,6 +1828,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1841,6 +1867,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1899,6 +1926,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), From 82390f6f7b887582449763ee8950389f1d01ac40 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 12 Aug 2025 19:27:27 +0200 Subject: [PATCH 178/247] Bump airOS to 0.2.8 (#150504) --- homeassistant/components/airos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airos/snapshots/test_diagnostics.ambr | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index 84003c19b8982..58f76abe57789 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.2.7"] + "requirements": ["airos==0.2.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index b9a87f17c338d..abc74ea56c066 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.7 +airos==0.2.8 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b0c118db552e..f9999b96e9ade 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.7 +airos==0.2.8 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr index 574dbf689497c..e3c4d74a5fda9 100644 --- a/tests/components/airos/snapshots/test_diagnostics.ambr +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -28,9 +28,14 @@ }), 'genuine': '/images/genuine.png', 'gps': dict({ + 'alt': None, + 'dim': None, + 'dop': None, 'fix': 0, 'lat': '**REDACTED**', 'lon': '**REDACTED**', + 'sats': None, + 'time_synced': None, }), 'host': dict({ 'cpuload': 10.10101, From 56b4c554def44ba8cf83c8d7567f961bf450cda1 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 12 Aug 2025 22:53:56 +0300 Subject: [PATCH 179/247] Bump aiowebostv to 0.7.5 (#150514) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index c3c3e9a564f04..f8201fe3bef29 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/webostv", "iot_class": "local_push", "loggers": ["aiowebostv"], - "requirements": ["aiowebostv==0.7.4"], + "requirements": ["aiowebostv==0.7.5"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/requirements_all.txt b/requirements_all.txt index abc74ea56c066..fb47514510d5d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,7 @@ aiowatttime==0.1.1 aiowebdav2==0.4.6 # homeassistant.components.webostv -aiowebostv==0.7.4 +aiowebostv==0.7.5 # homeassistant.components.withings aiowithings==3.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9999b96e9ade..700b04e8521b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ aiowatttime==0.1.1 aiowebdav2==0.4.6 # homeassistant.components.webostv -aiowebostv==0.7.4 +aiowebostv==0.7.5 # homeassistant.components.withings aiowithings==3.1.6 From 82907e5b882e39cfebd0430eb7ac231aa2dd5bb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Aug 2025 13:49:43 -0500 Subject: [PATCH 180/247] Bump bleak-retry-connector to 4.0.1 (#150515) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index ce5d98f8edbaa..d0d766862ff50 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "quality_scale": "internal", "requirements": [ "bleak==1.0.1", - "bleak-retry-connector==4.0.0", + "bleak-retry-connector==4.0.1", "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 601c0cf323864..d1f5ec4fefca3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ audioop-lts==0.2.1 av==13.1.0 awesomeversion==25.5.0 bcrypt==4.3.0 -bleak-retry-connector==4.0.0 +bleak-retry-connector==4.0.1 bleak==1.0.1 bluetooth-adapters==2.0.0 bluetooth-auto-recovery==1.5.2 diff --git a/requirements_all.txt b/requirements_all.txt index fb47514510d5d..3a54f4e7fea74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ bizkaibus==0.1.1 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.0.0 +bleak-retry-connector==4.0.1 # homeassistant.components.bluetooth bleak==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 700b04e8521b5..615ee3a6c8a0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ bimmer-connected[china]==0.17.2 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.0.0 +bleak-retry-connector==4.0.1 # homeassistant.components.bluetooth bleak==1.0.1 From 4213427b9c7f13f9925ba73f64ae925d6f7ed3b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Aug 2025 15:07:25 -0500 Subject: [PATCH 181/247] Bump aiodhcpwatcher to 1.2.1 (#150519) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 599e5ecae5ba3..32abe0684f71d 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==1.2.0", + "aiodhcpwatcher==1.2.1", "aiodiscover==2.7.1", "cached-ipaddress==0.10.0" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d1f5ec4fefca3..750d024a872f0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==1.2.0 +aiodhcpwatcher==1.2.1 aiodiscover==2.7.1 aiodns==3.5.0 aiohasupervisor==0.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index 3a54f4e7fea74..95c150298df41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ aiobotocore==2.21.1 aiocomelit==0.12.3 # homeassistant.components.dhcp -aiodhcpwatcher==1.2.0 +aiodhcpwatcher==1.2.1 # homeassistant.components.dhcp aiodiscover==2.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 615ee3a6c8a0f..40f2586501222 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,7 +208,7 @@ aiobotocore==2.21.1 aiocomelit==0.12.3 # homeassistant.components.dhcp -aiodhcpwatcher==1.2.0 +aiodhcpwatcher==1.2.1 # homeassistant.components.dhcp aiodiscover==2.7.1 From 776726a053c1134cce85430eb936daad343cd9c8 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 13 Aug 2025 05:38:18 -0400 Subject: [PATCH 182/247] Bump python-snoo to 0.8.1 (#150530) --- homeassistant/components/snoo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/snoo/const.py | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/snoo/manifest.json b/homeassistant/components/snoo/manifest.json index b47947ab0e019..0db11c5b086be 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.7.0"] + "requirements": ["python-snoo==0.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 95c150298df41..9cafe19d1e353 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2512,7 +2512,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.7.0 +python-snoo==0.8.1 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40f2586501222..7648e174fb099 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2082,7 +2082,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.7.0 +python-snoo==0.8.1 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/tests/components/snoo/const.py b/tests/components/snoo/const.py index 2657048afb8df..cd52679caf956 100644 --- a/tests/components/snoo/const.py +++ b/tests/components/snoo/const.py @@ -31,7 +31,12 @@ "name": "Test Snoo", "presence": {}, "presenceIoT": {}, - "awsIoT": {}, + "awsIoT": { + "awsRegion": "us-east-1", + "clientEndpoint": "z00023244d7fia4appr4b-ats.iot.us-east-1.amazonaws.com", + "clientReady": True, + "thingName": "676cbbe74529f85038b2e623_5831231335004715141_prod", + }, "lastSSID": {}, "provisionedAt": "random_time", } From 312d8aaff53cc38546a6bc43da764a423bfebc40 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 13 Aug 2025 09:45:54 +0200 Subject: [PATCH 183/247] Bump uv to 0.8.9 (#150542) --- Dockerfile | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 549837ddef054..4a004c046e350 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ RUN \ && go2rtc --version # Install uv -RUN pip3 install uv==0.7.1 +RUN pip3 install uv==0.8.9 WORKDIR /usr/src diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 750d024a872f0..8b38b7e66923f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -68,7 +68,7 @@ standard-telnetlib==3.13.0 typing-extensions>=4.14.0,<5.0 ulid-transform==1.4.0 urllib3>=2.0 -uv==0.7.1 +uv==0.8.9 voluptuous-openapi==0.1.0 voluptuous-serialize==2.6.0 voluptuous==0.15.2 diff --git a/pyproject.toml b/pyproject.toml index e869bd0d0cbf5..5757c965515a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ dependencies = [ "typing-extensions>=4.14.0,<5.0", "ulid-transform==1.4.0", "urllib3>=2.0", - "uv==0.7.1", + "uv==0.8.9", "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.1.0", diff --git a/requirements.txt b/requirements.txt index 7bd900a69ed05..f0f49ac519bd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,7 +46,7 @@ standard-telnetlib==3.13.0 typing-extensions>=4.14.0,<5.0 ulid-transform==1.4.0 urllib3>=2.0 -uv==0.7.1 +uv==0.8.9 voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.1.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 5776f6dfe1278..4b8aafce70ff4 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -14,7 +14,7 @@ WORKDIR "/github/workspace" COPY . /usr/src/homeassistant # Uv is only needed during build -RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ +RUN --mount=from=ghcr.io/astral-sh/uv:0.8.9,source=/uv,target=/bin/uv \ # Uv creates a lock file in /tmp --mount=type=tmpfs,target=/tmp \ # Required for PyTurboJPEG From 5a49007b86908cb71828545251f980faeeec1086 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 13 Aug 2025 18:12:18 -0400 Subject: [PATCH 184/247] Bump python-snoo to 0.8.2 (#150569) --- homeassistant/components/snoo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snoo/manifest.json b/homeassistant/components/snoo/manifest.json index 0db11c5b086be..0a2301c6fd84c 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.8.1"] + "requirements": ["python-snoo==0.8.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9cafe19d1e353..75e8b9c36d4e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2512,7 +2512,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.8.1 +python-snoo==0.8.2 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7648e174fb099..4276eb83b31ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2082,7 +2082,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.8.1 +python-snoo==0.8.2 # homeassistant.components.songpal python-songpal==0.16.2 From 1643d5df67cdd59a4bac24b7f336cdd7829081c4 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 13 Aug 2025 18:11:52 -0400 Subject: [PATCH 185/247] Change Snoo to use MQTT instead of PubNub (#150570) --- homeassistant/components/snoo/coordinator.py | 2 +- tests/components/snoo/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snoo/coordinator.py b/homeassistant/components/snoo/coordinator.py index 8ce0db3462125..43e717c2bc756 100644 --- a/homeassistant/components/snoo/coordinator.py +++ b/homeassistant/components/snoo/coordinator.py @@ -40,7 +40,7 @@ def __init__( async def setup(self) -> None: """Perform setup needed on every coordintaor creation.""" - await self.snoo.subscribe(self.device, self.async_set_updated_data) + self.snoo.start_subscribe(self.device, self.async_set_updated_data) # After we subscribe - get the status so that we have something to start with. # We only need to do this once. The device will auto update otherwise. await self.snoo.get_status(self.device) diff --git a/tests/components/snoo/__init__.py b/tests/components/snoo/__init__.py index b4692e6f08b83..417eb43814303 100644 --- a/tests/components/snoo/__init__.py +++ b/tests/components/snoo/__init__.py @@ -48,7 +48,7 @@ def find_update_callback( mock: AsyncMock, serial_number: str ) -> Callable[[SnooData], Awaitable[None]]: """Find the update callback for a specific identifier.""" - for call in mock.subscribe.call_args_list: + for call in mock.start_subscribe.call_args_list: if call[0][0].serialNumber == serial_number: return call[0][1] pytest.fail(f"Callback for identifier {serial_number} not found") From 2c1407f159140da76a385f1facca4ec499b9d7dd Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 14 Aug 2025 09:30:47 +0200 Subject: [PATCH 186/247] Make sure we update the api version in philips_js discovery (#150604) --- homeassistant/components/philips_js/config_flow.py | 2 +- tests/components/philips_js/test_config_flow.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index a568d51e5ea40..779452b284bf9 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -82,7 +82,7 @@ async def _async_attempt_prepare( ) await hub.getSystem() - await hub.setTransport(hub.secured_transport) + await hub.setTransport(hub.secured_transport, hub.api_version_detected) if not hub.system or not hub.name: raise ConnectionFailure("System data or name is empty") diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index c4dcc44e61926..77227fd0f635d 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -125,7 +125,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - mock_tv.setTransport.assert_called_with(True) + mock_tv.setTransport.assert_called_with(True, ANY) mock_tv.pairRequest.assert_called() result = await hass.config_entries.flow.async_configure( @@ -204,7 +204,7 @@ async def test_pair_grant_failed( assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - mock_tv.setTransport.assert_called_with(True) + mock_tv.setTransport.assert_called_with(True, ANY) mock_tv.pairRequest.assert_called() # Test with invalid pin @@ -266,6 +266,7 @@ async def test_zeroconf_discovery( """Test we can setup from zeroconf discovery.""" mock_tv_pairable.secured_transport = secured_transport + mock_tv_pairable.api_version_detected = 6 result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -291,7 +292,7 @@ async def test_zeroconf_discovery( assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - mock_tv_pairable.setTransport.assert_called_with(secured_transport) + mock_tv_pairable.setTransport.assert_called_with(secured_transport, 6) mock_tv_pairable.pairRequest.assert_called() From 87a2d3e6d97605b46dfcf92aabce4949f0b5153c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 7 Aug 2025 20:48:25 +0200 Subject: [PATCH 187/247] Bump pymiele to 0.5.3 (#150216) --- homeassistant/components/miele/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/manifest.json b/homeassistant/components/miele/manifest.json index c9a20e977f9c5..b8ca0535c3e95 100644 --- a/homeassistant/components/miele/manifest.json +++ b/homeassistant/components/miele/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_push", "loggers": ["pymiele"], "quality_scale": "bronze", - "requirements": ["pymiele==0.5.2"], + "requirements": ["pymiele==0.5.3"], "single_config_entry": true, "zeroconf": ["_mieleathome._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 75e8b9c36d4e7..6731133ffff54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2146,7 +2146,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.5.2 +pymiele==0.5.3 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4276eb83b31ea..8ba254d87f48d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1788,7 +1788,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.5.2 +pymiele==0.5.3 # homeassistant.components.mochad pymochad==0.2.0 From 1a0b61c98ecac2d554b228afe7506c71f12980d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 13 Aug 2025 23:42:47 +0200 Subject: [PATCH 188/247] Bump pymiele to 0.5.4 (#150605) --- homeassistant/components/miele/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/manifest.json b/homeassistant/components/miele/manifest.json index b8ca0535c3e95..63ace343dc89b 100644 --- a/homeassistant/components/miele/manifest.json +++ b/homeassistant/components/miele/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_push", "loggers": ["pymiele"], "quality_scale": "bronze", - "requirements": ["pymiele==0.5.3"], + "requirements": ["pymiele==0.5.4"], "single_config_entry": true, "zeroconf": ["_mieleathome._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 6731133ffff54..6080bb406c5e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2146,7 +2146,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.5.3 +pymiele==0.5.4 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ba254d87f48d..7a610241b68d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1788,7 +1788,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.5.3 +pymiele==0.5.4 # homeassistant.components.mochad pymochad==0.2.0 From 0b337c7e2aef1a1f3719ae3467b7a057a1723f65 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 14 Aug 2025 13:43:32 +0200 Subject: [PATCH 189/247] Bump airOS to 0.2.11 (#150627) --- homeassistant/components/airos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index 58f76abe57789..16855d805c05a 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.2.8"] + "requirements": ["airos==0.2.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6080bb406c5e7..271c0bc0407f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.8 +airos==0.2.11 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a610241b68d8..ebc36f7557c21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.8 +airos==0.2.11 # homeassistant.components.airthings_ble airthings-ble==0.9.2 From 837472c12d77f7a7ea29c1fc4c527ad4656874db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Aug 2025 16:16:04 -0500 Subject: [PATCH 190/247] Bump uiprotect to 7.21.1 (#150657) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 8eee080abb4a1..50bdeec8572ca 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.20.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.21.1", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 271c0bc0407f0..556fde7839fe6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3004,7 +3004,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.20.0 +uiprotect==7.21.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ebc36f7557c21..4277378445af6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2478,7 +2478,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.20.0 +uiprotect==7.21.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 83226ed01537319a05ed366ae8f6bb6d475c7e36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Aug 2025 22:49:31 -0500 Subject: [PATCH 191/247] Bump onvif-zeep-async to 4.0.3 (#150663) --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index fbb1454ec2a7f..787040d5691db 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==4.0.2", "WSDiscovery==2.1.2"] + "requirements": ["onvif-zeep-async==4.0.3", "WSDiscovery==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 556fde7839fe6..0af340488605c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1594,7 +1594,7 @@ ondilo==0.5.0 onedrive-personal-sdk==0.0.14 # homeassistant.components.onvif -onvif-zeep-async==4.0.2 +onvif-zeep-async==4.0.3 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4277378445af6..43a8d1135bde2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1362,7 +1362,7 @@ ondilo==0.5.0 onedrive-personal-sdk==0.0.14 # homeassistant.components.onvif -onvif-zeep-async==4.0.2 +onvif-zeep-async==4.0.3 # homeassistant.components.opengarage open-garage==0.2.0 From 06472224028470d5d7708de9227b7954f0e657ca Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 14 Aug 2025 23:46:59 -0400 Subject: [PATCH 192/247] Bump python-snoo to 0.8.3 (#150670) --- homeassistant/components/snoo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snoo/manifest.json b/homeassistant/components/snoo/manifest.json index 0a2301c6fd84c..5a162a9e9d3d2 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.8.2"] + "requirements": ["python-snoo==0.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0af340488605c..8543aaedeaaa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2512,7 +2512,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.8.2 +python-snoo==0.8.3 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43a8d1135bde2..718f7794e561b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2082,7 +2082,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.8.2 +python-snoo==0.8.3 # homeassistant.components.songpal python-songpal==0.16.2 From 22e19e768ec51adec7b1ea8f371a8efcc1533453 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Aug 2025 13:59:35 +0200 Subject: [PATCH 193/247] Fix missing labels for subdiv in workday (#150684) --- homeassistant/components/workday/config_flow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index 1d91e1d5ae385..20d9040e527a4 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -86,6 +86,9 @@ def add_province_and_language_to_schema( SelectOptionDict(value=k, label=", ".join(v)) for k, v in subdiv_aliases.items() ] + for option in province_options: + if option["label"] == "": + option["label"] = option["value"] else: province_options = provinces province_schema = { From c551a133c17eaaf09b15a28364391ba840bd64c4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Aug 2025 15:51:48 +0200 Subject: [PATCH 194/247] Improve handling decode errors in rest (#150699) --- homeassistant/components/rest/data.py | 16 ++++++-- tests/components/rest/test_data.py | 57 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 3341f296fb9ee..2964ef73d4614 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -45,6 +45,7 @@ def __init__( self._method = method self._resource = resource self._encoding = encoding + self._force_use_set_encoding = False # Convert auth tuple to aiohttp.BasicAuth if needed if isinstance(auth, tuple) and len(auth) == 2: @@ -152,10 +153,19 @@ async def async_update(self, log_errors: bool = True) -> None: # Read the response # Only use configured encoding if no charset in Content-Type header # If charset is present in Content-Type, let aiohttp use it - if response.charset: + if self._force_use_set_encoding is False and response.charset: # Let aiohttp use the charset from Content-Type header - self.data = await response.text() - else: + try: + self.data = await response.text() + except UnicodeDecodeError as ex: + self._force_use_set_encoding = True + _LOGGER.debug( + "Response charset came back as %s but could not be decoded, continue with configured encoding %s. %s", + response.charset, + self._encoding, + ex, + ) + if self._force_use_set_encoding or not response.charset: # Use configured encoding as fallback self.data = await response.text(encoding=self._encoding) self.headers = response.headers diff --git a/tests/components/rest/test_data.py b/tests/components/rest/test_data.py index 4d6bc000fac38..01581c8ac68f8 100644 --- a/tests/components/rest/test_data.py +++ b/tests/components/rest/test_data.py @@ -1,13 +1,17 @@ """Test REST data module logging improvements.""" +from datetime import timedelta import logging +from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.rest import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker @@ -89,6 +93,59 @@ async def test_rest_data_no_warning_on_200_with_wrong_content_type( ) +async def test_rest_data_with_incorrect_charset_in_header( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that we can handle sites which provides an incorrect charset.""" + aioclient_mock.get( + "http://example.com/api", + status=200, + text="

Some html

", + headers={"Content-Type": "text/html; charset=utf-8"}, + ) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "resource": "http://example.com/api", + "method": "GET", + "encoding": "windows-1250", + "sensor": [ + { + "name": "test_sensor", + "value_template": "{{ value }}", + } + ], + } + }, + ) + await hass.async_block_till_done() + + with patch( + "tests.test_util.aiohttp.AiohttpClientMockResponse.text", + side_effect=UnicodeDecodeError("utf-8", b"", 1, 0, ""), + ): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + log_text = "Response charset came back as utf-8 but could not be decoded, continue with configured encoding windows-1250." + assert log_text in caplog.text + + caplog.clear() + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Only log once as we only try once with automatic decoding + assert log_text not in caplog.text + + async def test_rest_data_no_warning_on_success_json( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, From 0bcc0f3fb93aeec096f4228e0afe25785b279cfd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Aug 2025 15:22:30 +0000 Subject: [PATCH 195/247] Bump version to 2025.8.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 c02668d6899d5..9ddbac360afff 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -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, 2) diff --git a/pyproject.toml b/pyproject.toml index 5757c965515a9..ced768ae63ea7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.1" +version = "2025.8.2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From c30d778a540bc1f4aaad86b6574e145bab7ec7c8 Mon Sep 17 00:00:00 2001 From: markhannon Date: Tue, 19 Aug 2025 22:40:12 +1000 Subject: [PATCH 196/247] Bump to zcc-helper==3.6 (#150608) --- homeassistant/components/zimi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zimi/manifest.json b/homeassistant/components/zimi/manifest.json index 3e019d2f053d5..58a56c978305a 100644 --- a/homeassistant/components/zimi/manifest.json +++ b/homeassistant/components/zimi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/zimi", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["zcc-helper==3.5.2"] + "requirements": ["zcc-helper==3.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8543aaedeaaa7..7a490e2a88fdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3194,7 +3194,7 @@ zabbix-utils==2.0.2 zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5.2 +zcc-helper==3.6 # homeassistant.components.zeroconf zeroconf==0.147.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 718f7794e561b..3cfcb3f3d1d80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2638,7 +2638,7 @@ yt-dlp[default]==2025.07.21 zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5.2 +zcc-helper==3.6 # homeassistant.components.zeroconf zeroconf==0.147.0 From 4e52826664cbbd4f421dc900a4b58691239a55b6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Aug 2025 15:59:33 +1000 Subject: [PATCH 197/247] fix(amberelectric): add request timeouts (#150613) Signed-off-by: JP-Ellis --- .../components/amberelectric/config_flow.py | 6 +++-- .../components/amberelectric/const.py | 2 ++ .../components/amberelectric/coordinator.py | 8 ++++-- .../amberelectric/test_coordinator.py | 26 ++++++++++++++----- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index c25258e2e3361..b5f034b44482b 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -16,7 +16,7 @@ SelectSelectorMode, ) -from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN +from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT API_URL = "https://app.amber.com.au/developers" @@ -64,7 +64,9 @@ def _fetch_sites(self, token: str) -> list[Site] | None: api = amberelectric.AmberApi(api_client) try: - sites: list[Site] = filter_sites(api.get_sites()) + sites: list[Site] = filter_sites( + api.get_sites(_request_timeout=REQUEST_TIMEOUT) + ) except amberelectric.ApiException as api_exception: if api_exception.status == 403: self._errors[CONF_API_TOKEN] = "invalid_api_token" diff --git a/homeassistant/components/amberelectric/const.py b/homeassistant/components/amberelectric/const.py index bdb9aa3186c4c..814b8a9bd6a06 100644 --- a/homeassistant/components/amberelectric/const.py +++ b/homeassistant/components/amberelectric/const.py @@ -22,3 +22,5 @@ GENERAL_CHANNEL = "general" CONTROLLED_LOAD_CHANNEL = "controlled_load" FEED_IN_CHANNEL = "feed_in" + +REQUEST_TIMEOUT = 15 diff --git a/homeassistant/components/amberelectric/coordinator.py b/homeassistant/components/amberelectric/coordinator.py index a1efef26aaeba..2ea14b5200b40 100644 --- a/homeassistant/components/amberelectric/coordinator.py +++ b/homeassistant/components/amberelectric/coordinator.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import LOGGER +from .const import LOGGER, REQUEST_TIMEOUT from .helpers import normalize_descriptor type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator] @@ -82,7 +82,11 @@ def update_price_data(self) -> dict[str, dict[str, Any]]: "grid": {}, } try: - data = self._api.get_current_prices(self.site_id, next=288) + data = self._api.get_current_prices( + self.site_id, + next=288, + _request_timeout=REQUEST_TIMEOUT, + ) intervals = [interval.actual_instance for interval in data] except ApiException as api_exception: raise UpdateFailed("Missing price data, skipping update") from api_exception diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index 0e82d81f4e8dc..b4557fb2a4dc0 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -15,7 +15,11 @@ from dateutil import parser import pytest -from homeassistant.components.amberelectric.const import CONF_SITE_ID, CONF_SITE_NAME +from homeassistant.components.amberelectric.const import ( + CONF_SITE_ID, + CONF_SITE_NAME, + REQUEST_TIMEOUT, +) from homeassistant.components.amberelectric.coordinator import AmberUpdateCoordinator from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant @@ -104,7 +108,9 @@ async def test_fetch_general_site(hass: HomeAssistant, current_price_api: Mock) result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -136,7 +142,9 @@ async def test_fetch_no_general_site( await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) @@ -150,7 +158,9 @@ async def test_fetch_api_error(hass: HomeAssistant, current_price_api: Mock) -> result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -201,7 +211,9 @@ async def test_fetch_general_and_controlled_load_site( result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_AND_CONTROLLED_SITE_ID, next=288 + GENERAL_AND_CONTROLLED_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -241,7 +253,9 @@ async def test_fetch_general_and_feed_in_site( result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_AND_FEED_IN_SITE_ID, next=288 + GENERAL_AND_FEED_IN_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance From 932c5ccf0f90921837a48c724bcc3a15c63a496e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:37:56 +0200 Subject: [PATCH 198/247] Bump renault-api to 0.4.0 (#150624) --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 2861c52c24ac0..9fe01c5b95296 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "silver", - "requirements": ["renault-api==0.3.1"] + "requirements": ["renault-api==0.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7a490e2a88fdb..2165c3a4aea96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2660,7 +2660,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.1 +renault-api==0.4.0 # homeassistant.components.renson renson-endura-delta==1.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3cfcb3f3d1d80..85e7cb6b91e44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2206,7 +2206,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.1 +renault-api==0.4.0 # homeassistant.components.renson renson-endura-delta==1.7.2 From 3dd091de4425dcace237677171483c9b7337367a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 16 Aug 2025 12:52:26 +0200 Subject: [PATCH 199/247] Update hassfest package exceptions (#150744) --- .github/workflows/ci.yaml | 2 +- script/hassfest/requirements.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ce7cf1ac12456..2dfd326ec8f09 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 4 + CACHE_VERSION: 5 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.8" diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 99a1c255e60f7..86dda1aab9aaf 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -83,7 +83,6 @@ # - reasonX should be the name of the invalid dependency "adax": {"adax": {"async-timeout"}, "adax-local": {"async-timeout"}}, "airthings": {"airthings-cloud": {"async-timeout"}}, - "alexa_devices": {"marisa-trie": {"setuptools"}}, "ampio": {"asmog": {"async-timeout"}}, "apache_kafka": {"aiokafka": {"async-timeout"}}, "apple_tv": {"pyatv": {"async-timeout"}}, From 122af46a926a0ca8970f6fc2523622bbe87e3a89 Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sun, 17 Aug 2025 00:37:44 +0200 Subject: [PATCH 200/247] Bump boschshcpy to 0.2.107 (#150754) --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index 0c99324efbbb3..bd2e127df3fe8 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/bosch_shc", "iot_class": "local_push", "loggers": ["boschshcpy"], - "requirements": ["boschshcpy==0.2.91"], + "requirements": ["boschshcpy==0.2.107"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 2165c3a4aea96..236bd0cf698cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ bond-async==0.2.1 bosch-alarm-mode2==0.4.6 # homeassistant.components.bosch_shc -boschshcpy==0.2.91 +boschshcpy==0.2.107 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85e7cb6b91e44..9b7ae230be5b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ bond-async==0.2.1 bosch-alarm-mode2==0.4.6 # homeassistant.components.bosch_shc -boschshcpy==0.2.91 +boschshcpy==0.2.107 # homeassistant.components.aws botocore==1.37.1 From 199b7e8ba7752c2410ab5f04315817ddeac6081f Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sun, 17 Aug 2025 10:49:04 +0200 Subject: [PATCH 201/247] Fix for bosch_shc: 'device_registry.async_get_or_create' referencing a non existing 'via_device' (#150756) --- homeassistant/components/bosch_shc/entity.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index 06ce45cdb3afd..e0e2963c340c8 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -69,12 +69,7 @@ def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: manufacturer=device.manufacturer, model=device.device_model, name=device.name, - via_device=( - DOMAIN, - device.parent_device_id - if device.parent_device_id is not None - else parent_id, - ), + via_device=(DOMAIN, device.root_device_id), ) super().__init__(device=device, parent_id=parent_id, entry_id=entry_id) From 332996cc38e94a8ee890f557abdbaea7ffcf605e Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sun, 17 Aug 2025 16:07:23 +0100 Subject: [PATCH 202/247] Fix volume step error in Squeezebox media player (#150760) --- homeassistant/components/squeezebox/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 839e419dd96cb..a857602a58411 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -326,7 +326,7 @@ async def async_will_remove_from_hass(self) -> None: def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" if self._player.volume is not None: - return int(float(self._player.volume)) / 100.0 + return float(self._player.volume) / 100.0 return None @@ -435,7 +435,7 @@ async def async_turn_off(self) -> None: async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - volume_percent = str(int(volume * 100)) + volume_percent = str(round(volume * 100)) await self._player.async_set_volume(volume_percent) await self.coordinator.async_refresh() From 27b32c5e930ffc653043b5f7667b507ed282b9cc Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:23:30 +0200 Subject: [PATCH 203/247] Show charging power as 0 when not charging for the Volvo integration (#150797) --- homeassistant/components/volvo/coordinator.py | 29 ++++- homeassistant/components/volvo/sensor.py | 4 +- tests/components/volvo/conftest.py | 4 +- .../ex30_2024/energy_capabilities.json | 4 +- .../fixtures/ex30_2024/energy_state.json | 4 +- .../energy_capabilities.json | 2 +- .../xc60_phev_2020/energy_capabilities.json | 2 +- .../fixtures/xc60_phev_2020/energy_state.json | 7 +- .../volvo/snapshots/test_sensor.ambr | 110 +++++++++++++++++- tests/components/volvo/test_sensor.py | 18 ++- 10 files changed, 168 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/volvo/coordinator.py b/homeassistant/components/volvo/coordinator.py index da23e7875c917..d6c8f349a52a2 100644 --- a/homeassistant/components/volvo/coordinator.py +++ b/homeassistant/components/volvo/coordinator.py @@ -260,6 +260,8 @@ def __init__( "Volvo medium interval coordinator", ) + self._supported_capabilities: list[str] = [] + async def _async_determine_api_calls( self, ) -> list[Callable[[], Coroutine[Any, Any, Any]]]: @@ -267,6 +269,31 @@ async def _async_determine_api_calls( capabilities = await self.api.async_get_energy_capabilities() if capabilities.get("isSupported", False): - return [self.api.async_get_energy_state] + self._supported_capabilities = [ + key + for key, value in capabilities.items() + if isinstance(value, dict) and value.get("isSupported", False) + ] + + return [self._async_get_energy_state] return [] + + async def _async_get_energy_state( + self, + ) -> dict[str, VolvoCarsValueStatusField | None]: + def _mark_ok( + field: VolvoCarsValueStatusField | None, + ) -> VolvoCarsValueStatusField | None: + if field: + field.status = "OK" + + return field + + energy_state = await self.api.async_get_energy_state() + + return { + key: _mark_ok(value) + for key, value in energy_state.items() + if key in self._supported_capabilities + } diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index a067549f0682a..bb20d64e17cee 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -67,8 +67,8 @@ def _calculate_time_to_service(field: VolvoCarsValue) -> int: def _charging_power_value(field: VolvoCarsValue) -> int: return ( - int(field.value) - if isinstance(field, VolvoCarsValueStatusField) and field.status == "OK" + field.value + if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int) else 0 ) diff --git a/tests/components/volvo/conftest.py b/tests/components/volvo/conftest.py index edd3f39998e9f..fedd3a6ec3f16 100644 --- a/tests/components/volvo/conftest.py +++ b/tests/components/volvo/conftest.py @@ -9,7 +9,7 @@ from volvocarsapi.models import ( VolvoCarsAvailableCommand, VolvoCarsLocation, - VolvoCarsValueField, + VolvoCarsValueStatusField, VolvoCarsVehicle, ) @@ -98,7 +98,7 @@ async def mock_api(hass: HomeAssistant, full_model: str) -> AsyncGenerator[Async hass, "energy_state", full_model ) energy_state = { - key: VolvoCarsValueField.from_dict(value) + key: VolvoCarsValueStatusField.from_dict(value) for key, value in energy_state_data.items() } engine_status = await async_load_fixture_as_value_field( diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json b/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json index 968c759ab2748..f3aff11585d6c 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { @@ -25,7 +25,7 @@ "isSupported": true }, "chargingCurrentLimit": { - "isSupported": true + "isSupported": false }, "chargingPower": { "isSupported": true diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_state.json b/tests/components/volvo/fixtures/ex30_2024/energy_state.json index 0170d1aa617f3..5973100d4ea8c 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_state.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_state.json @@ -50,7 +50,7 @@ }, "chargingPower": { "status": "ERROR", - "code": "NOT_SUPPORTED", - "message": "Resource is not supported for this vehicle" + "code": "PROPERTY_NOT_FOUND", + "message": "No valid value could be found for the requested property" } } diff --git a/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json b/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json index 968c759ab2748..3523d51e0717c 100644 --- a/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json +++ b/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json index d8aa07ff0bb65..331795f545b70 100644 --- a/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json index e2f0cd13807b0..e198bfc833095 100644 --- a/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json @@ -40,9 +40,10 @@ "message": "Resource is not supported for this vehicle" }, "targetBatteryChargeLevel": { - "status": "ERROR", - "code": "NOT_SUPPORTED", - "message": "Resource is not supported for this vehicle" + "status": "OK", + "value": 80, + "unit": "percentage", + "updatedAt": "2024-09-22T09:40:12Z" }, "chargingPower": { "status": "ERROR", diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index 29e7e1e72a5cf..cdc6b44ff79fc 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -232,6 +232,62 @@ 'state': 'connected', }) # --- +# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_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.volvo_ex30_charging_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging power', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_power', + 'unique_id': 'yv1abcdefg1234567_charging_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Volvo EX30 Charging power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_ex30_charging_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2164,7 +2220,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '1386', }) # --- # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_charging_power_status-entry] @@ -3601,6 +3657,58 @@ 'state': '30000', }) # --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_target_battery_charge_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target battery charge level', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'target_battery_charge_level', + 'unique_id': 'yv1abcdefg1234567_target_battery_charge_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC60 Target battery charge level', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_target_battery_charge_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- # name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/volvo/test_sensor.py b/tests/components/volvo/test_sensor.py index 2813c74128625..a4b7a787117ce 100644 --- a/tests/components/volvo/test_sensor.py +++ b/tests/components/volvo/test_sensor.py @@ -68,4 +68,20 @@ async def test_skip_invalid_api_fields( with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): assert await setup_integration() - assert not hass.states.get(f"sensor.volvo_{short_model}_charging_power") + assert not hass.states.get(f"sensor.volvo_{short_model}_charging_current_limit") + + +@pytest.mark.parametrize( + "full_model", + ["ex30_2024"], +) +async def test_charging_power_value( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], +) -> None: + """Test if charging_power_value is zero if supported, but not charging.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): + assert await setup_integration() + + assert hass.states.get("sensor.volvo_ex30_charging_power").state == "0" From 38aba81f62188b38a5703a2fd20a5e246eeabdb3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 17 Aug 2025 19:27:17 +0200 Subject: [PATCH 204/247] Pin gql to 3.5.3 (#150800) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b38b7e66923f..dd3cb9500a479 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -221,3 +221,6 @@ num2words==0.5.14 # downgraded or upgraded by custom components # This ensures all use the same version pymodbus==3.9.2 + +# Some packages don't support gql 4.0.0 yet +gql<4.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index eb986fd8bb0ab..2855d7998c15c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -247,6 +247,9 @@ # downgraded or upgraded by custom components # This ensures all use the same version pymodbus==3.9.2 + +# Some packages don't support gql 4.0.0 yet +gql<4.0.0 """ GENERATED_MESSAGE = ( From 81377be92f97725735521006b21a8391fe54f9d1 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 17 Aug 2025 14:48:14 -0700 Subject: [PATCH 205/247] Bump opower to 0.15.2 (#150809) --- 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 a10c5b2d15d4a..e127824ac1919 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.15.1"] + "requirements": ["opower==0.15.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 236bd0cf698cb..66f45b1f0ac8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1628,7 +1628,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.15.1 +opower==0.15.2 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b7ae230be5b4..04b82d90cfce6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1384,7 +1384,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.15.1 +opower==0.15.2 # homeassistant.components.oralb oralb-ble==0.17.6 From 1ca6c4b5b878fe58b30bf90808df9c47a9d57b30 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 18 Aug 2025 10:04:54 +0200 Subject: [PATCH 206/247] Include device data in Withings diagnostics (#150816) --- .../components/withings/diagnostics.py | 11 ++++++ .../withings/snapshots/test_diagnostics.ambr | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/homeassistant/components/withings/diagnostics.py b/homeassistant/components/withings/diagnostics.py index d8b590753680b..dd154488be285 100644 --- a/homeassistant/components/withings/diagnostics.py +++ b/homeassistant/components/withings/diagnostics.py @@ -2,16 +2,23 @@ from __future__ import annotations +from dataclasses import asdict from typing import Any from yarl import URL +from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.webhook import async_generate_url as webhook_generate_url from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from . import CONF_CLOUDHOOK_URL, WithingsConfigEntry +TO_REDACT = { + "device_id", + "hashed_device_id", +} + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: WithingsConfigEntry @@ -53,4 +60,8 @@ async def async_get_config_entry_diagnostics( "received_sleep_data": withings_data.sleep_coordinator.data is not None, "received_workout_data": withings_data.workout_coordinator.data is not None, "received_activity_data": withings_data.activity_coordinator.data is not None, + "devices": async_redact_data( + [asdict(v) for v in withings_data.device_coordinator.data.values()], + TO_REDACT, + ), } diff --git a/tests/components/withings/snapshots/test_diagnostics.ambr b/tests/components/withings/snapshots/test_diagnostics.ambr index f7c704a2c49e1..bfd56fbc4d47f 100644 --- a/tests/components/withings/snapshots/test_diagnostics.ambr +++ b/tests/components/withings/snapshots/test_diagnostics.ambr @@ -1,6 +1,18 @@ # serializer version: 1 # name: test_diagnostics_cloudhook_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': True, 'has_valid_external_webhook_url': True, 'received_activity_data': False, @@ -64,6 +76,18 @@ # --- # name: test_diagnostics_polling_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': False, 'has_valid_external_webhook_url': False, 'received_activity_data': False, @@ -127,6 +151,18 @@ # --- # name: test_diagnostics_webhook_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': False, 'has_valid_external_webhook_url': True, 'received_activity_data': False, From 92b988a292c8a34abb2d9f89a794120764f14180 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 18 Aug 2025 09:41:37 +0200 Subject: [PATCH 207/247] Abort Nanoleaf discovery flows with user flow (#150818) --- .../components/nanoleaf/config_flow.py | 11 +++- tests/components/nanoleaf/test_config_flow.py | 57 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 253387c254afc..d62168a4ad37a 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -10,7 +10,12 @@ from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_USER, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.json import save_json @@ -200,7 +205,9 @@ async def async_setup_finish( return self.async_abort(reason="unknown") name = self.nanoleaf.name - await self.async_set_unique_id(name) + await self.async_set_unique_id( + name, raise_on_progress=self.source != SOURCE_USER + ) self._abort_if_unique_id_configured({CONF_HOST: self.nanoleaf.host}) if discovery_integration_import: diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index ba89405bc9793..d9616572b2ea6 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.nanoleaf.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -463,3 +464,59 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_abort_discovery_flow_with_user_flow(hass: HomeAssistant) -> None: + """Test abort discovery flow if user flow is already in progress.""" + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.load_json_object", + return_value={}, + ), + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf", + return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), + ), + patch( + "homeassistant.components.nanoleaf.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={}, + ssdp_headers={ + "_host": TEST_HOST, + "nl-devicename": TEST_NAME, + "nl-deviceid": TEST_DEVICE_ID, + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] is None + assert result["step_id"] == "link" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # Verify the discovery flow was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) From 4b2a14907203fc502941d2ea250d4110befb586c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 18 Aug 2025 09:39:08 +0200 Subject: [PATCH 208/247] Bump yt-dlp to 2025.08.11 (#150821) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index db622d21f1a12..477e77022de74 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.07.21"], + "requirements": ["yt-dlp[default]==2025.08.11"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 66f45b1f0ac8e..5f31624fe0191 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3185,7 +3185,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.07.21 +yt-dlp[default]==2025.08.11 # homeassistant.components.zabbix zabbix-utils==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04b82d90cfce6..a34abb9fd8149 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2632,7 +2632,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.07.21 +yt-dlp[default]==2025.08.11 # homeassistant.components.zamg zamg==0.3.6 From 7639e12ff2e242520eb837c26507a9e1667c73b1 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Thu, 21 Aug 2025 20:40:22 +0900 Subject: [PATCH 209/247] Initialize the coordinator's data to include data.options. (#150839) Co-authored-by: yunseon.park --- homeassistant/components/lg_thinq/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lg_thinq/coordinator.py b/homeassistant/components/lg_thinq/coordinator.py index 9f84c4222772b..ffdde3188db6f 100644 --- a/homeassistant/components/lg_thinq/coordinator.py +++ b/homeassistant/components/lg_thinq/coordinator.py @@ -37,7 +37,7 @@ def __init__( name=f"{DOMAIN}_{ha_bridge.device.device_id}", ) - self.data = {} + self.data = ha_bridge.update_status(None) self.api = ha_bridge self.device_id = ha_bridge.device.device_id self.sub_id = ha_bridge.sub_id From fe71b54c3e71a1f5f69dcf7970eaca4d8c940869 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 18 Aug 2025 22:14:52 +0200 Subject: [PATCH 210/247] Handle Z-Wave RssiErrorReceived (#150846) --- homeassistant/components/zwave_js/sensor.py | 38 +++-- tests/components/zwave_js/test_sensor.py | 177 ++++++++++++++++++++ 2 files changed, 204 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 2efb8c8e67c64..23b906a9d1607 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -4,15 +4,15 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import Any +from typing import Any, cast import voluptuous as vol -from zwave_js_server.const import CommandClass +from zwave_js_server.const import CommandClass, RssiError from zwave_js_server.const.command_class.meter import ( RESET_METER_OPTION_TARGET_VALUE, RESET_METER_OPTION_TYPE, ) -from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.exceptions import BaseZwaveJSServerError, RssiErrorReceived from zwave_js_server.model.controller import Controller from zwave_js_server.model.controller.statistics import ControllerStatistics from zwave_js_server.model.driver import Driver @@ -1049,7 +1049,7 @@ def __init__( self, config_entry: ZwaveJSConfigEntry, driver: Driver, - statistics_src: ZwaveNode | Controller, + statistics_src: Controller | ZwaveNode, description: ZWaveJSStatisticsSensorEntityDescription, ) -> None: """Initialize a Z-Wave statistics entity.""" @@ -1080,13 +1080,31 @@ async def async_poll_value(self, _: bool) -> None: ) @callback - def statistics_updated(self, event_data: dict) -> None: + def _statistics_updated(self, event_data: dict) -> None: """Call when statistics updated event is received.""" - self._attr_native_value = self.entity_description.convert( - event_data["statistics_updated"], self.entity_description.key + statistics = cast( + ControllerStatistics | NodeStatistics, event_data["statistics_updated"] ) + self._set_statistics(statistics) self.async_write_ha_state() + @callback + def _set_statistics( + self, statistics: ControllerStatistics | NodeStatistics + ) -> None: + """Set updated statistics.""" + try: + self._attr_native_value = self.entity_description.convert( + statistics, self.entity_description.key + ) + except RssiErrorReceived as err: + if err.error is RssiError.NOT_AVAILABLE: + self._attr_available = False + return + self._attr_native_value = None + # Reset available state. + self._attr_available = True + async def async_added_to_hass(self) -> None: """Call when entity is added.""" self.async_on_remove( @@ -1104,10 +1122,8 @@ async def async_added_to_hass(self) -> None: ) ) self.async_on_remove( - self.statistics_src.on("statistics updated", self.statistics_updated) + self.statistics_src.on("statistics updated", self._statistics_updated) ) # Set initial state - self._attr_native_value = self.entity_description.convert( - self.statistics_src.statistics, self.entity_description.key - ) + self._set_statistics(self.statistics_src.statistics) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index c7b41449d43df..e287c9e988fcf 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1045,6 +1045,183 @@ async def test_last_seen_statistics_sensors( assert state.state == "2024-01-01T12:00:00+00:00" +async def test_rssi_sensor_error( + hass: HomeAssistant, + zp3111: Node, + integration: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test rssi sensor error.""" + entity_id = "sensor.4_in_1_sensor_signal_strength" + + entity_registry.async_update_entity(entity_id, disabled_by=None) + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + # Fire statistics updated event for node + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 7, # baseline + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "7" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 125, # no signal detected + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 127, # not available + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unavailable" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 126, # receiver saturated + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + ENERGY_PRODUCTION_ENTITY_MAP = { "energy_production_power": { "state": 1.23, From 59d73138e77324ee7a5a53a4260a9adf7c415352 Mon Sep 17 00:00:00 2001 From: Imeon-Energy Date: Tue, 19 Aug 2025 10:19:03 +0200 Subject: [PATCH 211/247] Use correct unit and class for the Imeon inverter sensors (#150847) Co-authored-by: TheBushBoy --- .../components/imeon_inverter/sensor.py | 21 +++++----- .../imeon_inverter/snapshots/test_sensor.ambr | 40 +++++++++---------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/imeon_inverter/sensor.py b/homeassistant/components/imeon_inverter/sensor.py index 32d40923fa14b..119677c0a8a3c 100644 --- a/homeassistant/components/imeon_inverter/sensor.py +++ b/homeassistant/components/imeon_inverter/sensor.py @@ -14,7 +14,6 @@ EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, - UnitOfEnergy, UnitOfFrequency, UnitOfPower, UnitOfTemperature, @@ -50,8 +49,8 @@ SensorEntityDescription( key="battery_stored", translation_key="battery_stored", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY_STORAGE, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), # Grid @@ -238,16 +237,16 @@ SensorEntityDescription( key="pv_consumed", translation_key="pv_consumed", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="pv_injected", translation_key="pv_injected", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="pv_power_1", @@ -290,14 +289,14 @@ key="monitoring_self_consumption", translation_key="monitoring_self_consumption", native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, suggested_display_precision=2, ), SensorEntityDescription( key="monitoring_self_sufficiency", translation_key="monitoring_self_sufficiency", native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, suggested_display_precision=2, ), # Monitoring (instant minute data) diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index fb59aa9dedef6..84e691bc8de2d 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -192,7 +192,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'Battery stored', 'platform': 'imeon_inverter', @@ -201,16 +201,16 @@ 'supported_features': 0, 'translation_key': 'battery_stored', 'unique_id': '111111111111111_battery_stored', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_battery_stored-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy_storage', + 'device_class': 'power', 'friendly_name': 'Imeon inverter Battery stored', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_battery_stored', @@ -1290,7 +1290,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1328,7 +1328,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Imeon inverter Monitoring self-consumption', - 'state_class': , + 'state_class': , 'unit_of_measurement': '%', }), 'context': , @@ -1345,7 +1345,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1383,7 +1383,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Imeon inverter Monitoring self-sufficiency', - 'state_class': , + 'state_class': , 'unit_of_measurement': '%', }), 'context': , @@ -2072,7 +2072,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -2094,7 +2094,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'PV consumed', 'platform': 'imeon_inverter', @@ -2103,16 +2103,16 @@ 'supported_features': 0, 'translation_key': 'pv_consumed', 'unique_id': '111111111111111_pv_consumed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_pv_consumed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy', + 'device_class': 'power', 'friendly_name': 'Imeon inverter PV consumed', - 'state_class': , - 'unit_of_measurement': , + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_pv_consumed', @@ -2128,7 +2128,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -2150,7 +2150,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'PV injected', 'platform': 'imeon_inverter', @@ -2159,16 +2159,16 @@ 'supported_features': 0, 'translation_key': 'pv_injected', 'unique_id': '111111111111111_pv_injected', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_pv_injected-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy', + 'device_class': 'power', 'friendly_name': 'Imeon inverter PV injected', - 'state_class': , - 'unit_of_measurement': , + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_pv_injected', From 945771098e22e3f5b364cc2c85226c57ded83265 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 18 Aug 2025 21:40:43 +0200 Subject: [PATCH 212/247] Bump holidays to 0.79 (#150857) --- 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 dde50da1af3d7..5ea0d217f141f 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.78", "babel==2.15.0"] + "requirements": ["holidays==0.79", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index d230970272865..0e336632b2ebf 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.78"] + "requirements": ["holidays==0.79"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5f31624fe0191..f9841fcb1a8f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1171,7 +1171,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.78 +holidays==0.79 # homeassistant.components.frontend home-assistant-frontend==20250811.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a34abb9fd8149..d06f79851a5de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1020,7 +1020,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.78 +holidays==0.79 # homeassistant.components.frontend home-assistant-frontend==20250811.0 From a3f5c3f422ff1a711de1fb863dad97e010443d27 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Tue, 19 Aug 2025 02:45:42 -0500 Subject: [PATCH 213/247] Bump aiorussound to 4.8.1 (#150858) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index aad9b9425aad6..efaf8f195adc3 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==4.8.0"], + "requirements": ["aiorussound==4.8.1"], "zeroconf": ["_rio._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index f9841fcb1a8f7..59f8c494e76df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -375,7 +375,7 @@ aioridwell==2024.01.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.8.0 +aiorussound==4.8.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d06f79851a5de..31dc2ec30805b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -357,7 +357,7 @@ aioridwell==2024.01.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.8.0 +aiorussound==4.8.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From d1698222f444fda53af9c378e85201c0ab446803 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 19 Aug 2025 16:55:16 +0200 Subject: [PATCH 214/247] Add missing unsupported reasons to list (#150866) --- homeassistant/components/hassio/issues.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 35f7f48481e2c..b037973041b5e 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -61,18 +61,19 @@ UNSUPPORTED_REASONS = { "apparmor", + "cgroup_version", "connectivity_check", "content_trust", "dbus", "dns_server", "docker_configuration", "docker_version", - "cgroup_version", "job_conditions", "lxc", "network_manager", "os", "os_agent", + "os_version", "restart_policy", "software", "source_mods", @@ -80,6 +81,7 @@ "systemd", "systemd_journal", "systemd_resolved", + "virtualization_image", } # Some unsupported reasons also mark the system as unhealthy. If the unsupported reason # provides no additional information beyond the unhealthy one then skip that repair. From cb8669c84f09ab0f2d6e2a664c14f13d7b8d2cfc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:19:10 +0200 Subject: [PATCH 215/247] Fix icloud service calls (#150881) --- homeassistant/components/icloud/services.py | 25 +++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/icloud/services.py b/homeassistant/components/icloud/services.py index dbb843e821681..44a2e5d52f7a6 100644 --- a/homeassistant/components/icloud/services.py +++ b/homeassistant/components/icloud/services.py @@ -8,7 +8,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import slugify -from .account import IcloudAccount +from .account import IcloudAccount, IcloudConfigEntry from .const import ( ATTR_ACCOUNT, ATTR_DEVICE_NAME, @@ -92,8 +92,10 @@ def lost_device(service: ServiceCall) -> None: def update_account(service: ServiceCall) -> None: """Call the update function of an iCloud account.""" if (account := service.data.get(ATTR_ACCOUNT)) is None: - for account in service.hass.data[DOMAIN].values(): - account.keep_alive() + # Update all accounts when no specific account is provided + entry: IcloudConfigEntry + for entry in service.hass.config_entries.async_loaded_entries(DOMAIN): + entry.runtime_data.keep_alive() else: _get_account(service.hass, account).keep_alive() @@ -102,17 +104,12 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount: if account_identifier is None: return None - icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) - if icloud_account is None: - for account in hass.data[DOMAIN].values(): - if account.username == account_identifier: - icloud_account = account - - if icloud_account is None: - raise ValueError( - f"No iCloud account with username or name {account_identifier}" - ) - return icloud_account + entry: IcloudConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + if entry.runtime_data.username == account_identifier: + return entry.runtime_data + + raise ValueError(f"No iCloud account with username or name {account_identifier}") @callback From 6383f9365c76e9938c957ae557b619f5185f0fea Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 20 Aug 2025 07:45:35 +0200 Subject: [PATCH 216/247] Bump pysmartthings to 3.2.9 (#150892) --- 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 35354570f232b..951d1372a6991 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.8"] + "requirements": ["pysmartthings==3.2.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 59f8c494e76df..962733d48b468 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2352,7 +2352,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.1 # homeassistant.components.smartthings -pysmartthings==3.2.8 +pysmartthings==3.2.9 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31dc2ec30805b..fa6b986206e1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1955,7 +1955,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.1 # homeassistant.components.smartthings -pysmartthings==3.2.8 +pysmartthings==3.2.9 # homeassistant.components.smarty pysmarty2==0.10.2 From 0cd28e7fc1a30e0645dc8266336b6f95e4850175 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Aug 2025 16:24:32 +0200 Subject: [PATCH 217/247] Fix PWA theme color to match darker blue color scheme in 2025.8 (#150896) Co-authored-by: Claude --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2f2a8e93b1e0d..ff50567257ac5 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -50,7 +50,7 @@ CONF_FRONTEND_REPO = "development_repo" CONF_JS_VERSION = "javascript_version" -DEFAULT_THEME_COLOR = "#03A9F4" +DEFAULT_THEME_COLOR = "#2980b9" DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels") From 9414356a4d1fabfc83b79d5909f66b5596fdf921 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 12:21:37 -0500 Subject: [PATCH 218/247] Bump bleak-retry-connector to 4.0.2 (#150899) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index d0d766862ff50..7304b8828e1df 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "quality_scale": "internal", "requirements": [ "bleak==1.0.1", - "bleak-retry-connector==4.0.1", + "bleak-retry-connector==4.0.2", "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dd3cb9500a479..e04f73bc42534 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ audioop-lts==0.2.1 av==13.1.0 awesomeversion==25.5.0 bcrypt==4.3.0 -bleak-retry-connector==4.0.1 +bleak-retry-connector==4.0.2 bleak==1.0.1 bluetooth-adapters==2.0.0 bluetooth-auto-recovery==1.5.2 diff --git a/requirements_all.txt b/requirements_all.txt index 962733d48b468..ab1e24225645f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ bizkaibus==0.1.1 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.0.1 +bleak-retry-connector==4.0.2 # homeassistant.components.bluetooth bleak==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa6b986206e1e..2c6cfed150399 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ bimmer-connected[china]==0.17.2 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.0.1 +bleak-retry-connector==4.0.2 # homeassistant.components.bluetooth bleak==1.0.1 From e4329ab8a56b4a54a53d1836214b8654615d2a6b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 20 Aug 2025 07:46:51 +0200 Subject: [PATCH 219/247] update pyatmo to v9.2.3 (#150900) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 595c57b1b4be0..aeb4ffa0c55ee 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==9.2.1"] + "requirements": ["pyatmo==9.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index ab1e24225645f..90bb5d15ad698 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1852,7 +1852,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==9.2.1 +pyatmo==9.2.3 # homeassistant.components.apple_tv pyatv==0.16.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c6cfed150399..385f5be0b1551 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1557,7 +1557,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==9.2.1 +pyatmo==9.2.3 # homeassistant.components.apple_tv pyatv==0.16.1 From 1bd5aa0ab0e6991c1574cc5d7216484df0db030f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Aug 2025 22:17:30 +0200 Subject: [PATCH 220/247] Fix structured output object selector conversion for OpenAI (#150916) --- homeassistant/components/openai_conversation/entity.py | 4 ++-- tests/components/openai_conversation/test_entity.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openai_conversation/entity.py b/homeassistant/components/openai_conversation/entity.py index 748c0c8f87441..885832bb4ca28 100644 --- a/homeassistant/components/openai_conversation/entity.py +++ b/homeassistant/components/openai_conversation/entity.py @@ -91,6 +91,8 @@ def _adjust_schema(schema: dict[str, Any]) -> None: """Adjust the schema to be compatible with OpenAI API.""" if schema["type"] == "object": + schema.setdefault("strict", True) + schema.setdefault("additionalProperties", False) if "properties" not in schema: return @@ -124,8 +126,6 @@ def _format_structured_output( _adjust_schema(result) - result["strict"] = True - result["additionalProperties"] = False return result diff --git a/tests/components/openai_conversation/test_entity.py b/tests/components/openai_conversation/test_entity.py index 58187bd63e98d..c24cb5b3d799b 100644 --- a/tests/components/openai_conversation/test_entity.py +++ b/tests/components/openai_conversation/test_entity.py @@ -63,6 +63,8 @@ async def test_format_structured_output() -> None: "item_value", ], "type": "object", + "additionalProperties": False, + "strict": True, }, "type": "array", }, From bb9660269cc77d81dd8f071a878b0331bd3713d1 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Thu, 21 Aug 2025 10:19:14 -0400 Subject: [PATCH 221/247] Matter valve Open command doesn't support TargetLevel=0 (#150922) --- homeassistant/components/matter/valve.py | 9 ++++++--- tests/components/matter/test_valve.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/valve.py b/homeassistant/components/matter/valve.py index bea11468c6b95..4cedec74bf24b 100644 --- a/homeassistant/components/matter/valve.py +++ b/homeassistant/components/matter/valve.py @@ -52,9 +52,12 @@ async def async_close_valve(self) -> None: async def async_set_valve_position(self, position: int) -> None: """Move the valve to a specific position.""" - await self.send_device_command( - ValveConfigurationAndControl.Commands.Open(targetLevel=position) - ) + if position > 0: + await self.send_device_command( + ValveConfigurationAndControl.Commands.Open(targetLevel=position) + ) + return + await self.send_device_command(ValveConfigurationAndControl.Commands.Close()) @callback def _update_from_device(self) -> None: diff --git a/tests/components/matter/test_valve.py b/tests/components/matter/test_valve.py index 36ab34cb64e60..db64a5bacefa6 100644 --- a/tests/components/matter/test_valve.py +++ b/tests/components/matter/test_valve.py @@ -133,3 +133,22 @@ async def test_valve( command=clusters.ValveConfigurationAndControl.Commands.Open(targetLevel=100), ) matter_client.send_device_command.reset_mock() + + # test using set_position action to close valve + await hass.services.async_call( + "valve", + "set_valve_position", + { + "entity_id": entity_id, + "position": 0, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.ValveConfigurationAndControl.Commands.Close(), + ) + matter_client.send_device_command.reset_mock() From 2e7821d64a021d878ce4bf3a22359ffa0451ae0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Aug 2025 02:25:17 -0500 Subject: [PATCH 222/247] Bump ESPHome minimum stable BLE version to 2025.8.0 (#150924) --- homeassistant/components/esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py index 2c9bee3273498..385c88d6eb9e9 100644 --- a/homeassistant/components/esphome/const.py +++ b/homeassistant/components/esphome/const.py @@ -17,7 +17,7 @@ DEFAULT_PORT: Final = 6053 -STABLE_BLE_VERSION_STR = "2025.5.0" +STABLE_BLE_VERSION_STR = "2025.8.0" STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) PROJECT_URLS = { "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", From 9194ddd4fe2c3bd592b5d98f8c8f81d2ca96234d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 20 Aug 2025 13:08:47 +0200 Subject: [PATCH 223/247] Bump imgw-pib to version 1.5.4 (#150930) --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 145690487d7c0..b0779b35f1474 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["imgw_pib==1.5.3"] + "requirements": ["imgw_pib==1.5.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 90bb5d15ad698..4fca15d61db62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1240,7 +1240,7 @@ ihcsdk==2.8.5 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.3 +imgw_pib==1.5.4 # homeassistant.components.incomfort incomfort-client==0.6.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 385f5be0b1551..064cd60772e74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1074,7 +1074,7 @@ igloohome-api==0.1.1 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.3 +imgw_pib==1.5.4 # homeassistant.components.incomfort incomfort-client==0.6.9 From add75e06e30cbe00110623da13a6ece256574175 Mon Sep 17 00:00:00 2001 From: Imeon-Energy Date: Thu, 21 Aug 2025 13:31:06 +0200 Subject: [PATCH 224/247] Fix update retry for Imeon inverter integration (#150936) Co-authored-by: TheBushBoy --- homeassistant/components/imeon_inverter/coordinator.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/imeon_inverter/coordinator.py b/homeassistant/components/imeon_inverter/coordinator.py index f1963a45579d1..02e81927005c0 100644 --- a/homeassistant/components/imeon_inverter/coordinator.py +++ b/homeassistant/components/imeon_inverter/coordinator.py @@ -75,13 +75,11 @@ async def _async_update_data(self) -> dict[str, str | float | int]: data: dict[str, str | float | int] = {} async with timeout(TIMEOUT): - await self._api.login( - self.config_entry.data[CONF_USERNAME], - self.config_entry.data[CONF_PASSWORD], - ) - - # Fetch data using distant API try: + await self._api.login( + self.config_entry.data[CONF_USERNAME], + self.config_entry.data[CONF_PASSWORD], + ) await self._api.update() except (ValueError, ClientError) as e: raise UpdateFailed(e) from e From 2f4e29ba718a58f39d1120ba493a93c28943f67e Mon Sep 17 00:00:00 2001 From: elsi06 Date: Thu, 21 Aug 2025 15:48:03 +0200 Subject: [PATCH 225/247] Bump python-mystrom to 2.5.0 (#150947) --- homeassistant/components/mystrom/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index c5a981dbf460b..fa03370004303 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mystrom", "iot_class": "local_polling", "loggers": ["pymystrom"], - "requirements": ["python-mystrom==2.4.0"] + "requirements": ["python-mystrom==2.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4fca15d61db62..cce08a7e363d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2478,7 +2478,7 @@ python-miio==0.5.12 python-mpd2==3.1.1 # homeassistant.components.mystrom -python-mystrom==2.4.0 +python-mystrom==2.5.0 # homeassistant.components.open_router python-open-router==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 064cd60772e74..b0d2b71fed83b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2051,7 +2051,7 @@ python-miio==0.5.12 python-mpd2==3.1.1 # homeassistant.components.mystrom -python-mystrom==2.4.0 +python-mystrom==2.5.0 # homeassistant.components.open_router python-open-router==0.3.1 From 2dad6fa298420fdcfb6f2bd8901bf74e51676c1a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 21 Aug 2025 14:44:19 +0200 Subject: [PATCH 226/247] Ask user for Z-Wave RF region if country is missing (#150959) Co-authored-by: Paulus Schoutsen Co-authored-by: TheJulianJES --- .../components/zwave_js/config_flow.py | 70 +++- .../components/zwave_js/strings.json | 10 + tests/components/zwave_js/test_config_flow.py | 332 ++++++++++++++++++ 3 files changed, 408 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index b72a71279ab49..92912a2cdb584 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -35,6 +35,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import selector from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.usb import UsbServiceInfo @@ -88,6 +89,8 @@ CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY, } +CONF_ADDON_RF_REGION = "rf_region" + EXAMPLE_SERVER_URL = "ws://localhost:3000" ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61") @@ -103,6 +106,19 @@ "#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui" ) +RF_REGIONS = [ + "Australia/New Zealand", + "China", + "Europe", + "Hong Kong", + "India", + "Israel", + "Japan", + "Korea", + "Russia", + "USA", +] + def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: """Return a schema for the manual step.""" @@ -195,10 +211,12 @@ def __init__(self) -> None: self.backup_data: bytes | None = None self.backup_filepath: Path | None = None self.use_addon = False + self._addon_config_updates: dict[str, Any] = {} self._migrating = False self._reconfigure_config_entry: ZwaveJSConfigEntry | None = None self._usb_discovery = False self._recommended_install = False + self._rf_region: str | None = None async def async_step_install_addon( self, user_input: dict[str, Any] | None = None @@ -236,6 +254,21 @@ async def async_step_start_addon( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Start Z-Wave JS add-on.""" + if self.hass.config.country is None and ( + not self._rf_region or self._rf_region == "Automatic" + ): + # If the country is not set, we need to check the RF region add-on config. + addon_info = await self._async_get_addon_info() + rf_region: str | None = addon_info.options.get(CONF_ADDON_RF_REGION) + self._rf_region = rf_region + if rf_region is None or rf_region == "Automatic": + # If the RF region is not set, we need to ask the user to select it. + return await self.async_step_rf_region() + if config_updates := self._addon_config_updates: + # If we have updates to the add-on config, set them before starting the add-on. + self._addon_config_updates = {} + await self._async_set_addon_config(config_updates) + if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) @@ -629,6 +662,33 @@ async def async_step_intent_custom( return await self.async_step_on_supervisor({CONF_USE_ADDON: True}) return await self.async_step_on_supervisor() + async def async_step_rf_region( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle RF region selection step.""" + if user_input is not None: + # Store the selected RF region + self._addon_config_updates[CONF_ADDON_RF_REGION] = self._rf_region = ( + user_input["rf_region"] + ) + return await self.async_step_start_addon() + + schema = vol.Schema( + { + vol.Required("rf_region"): selector.SelectSelector( + selector.SelectSelectorConfig( + options=RF_REGIONS, + mode=selector.SelectSelectorMode.DROPDOWN, + ) + ), + } + ) + + return self.async_show_form( + step_id="rf_region", + data_schema=schema, + ) + async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -728,7 +788,7 @@ async def async_step_network_type( CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } - await self._async_set_addon_config(addon_config_updates) + self._addon_config_updates = addon_config_updates return await self.async_step_start_addon() # Network already exists, go to security keys step @@ -799,7 +859,7 @@ async def async_step_configure_security_keys( CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } - await self._async_set_addon_config(addon_config_updates) + self._addon_config_updates = addon_config_updates return await self.async_step_start_addon() data_schema = vol.Schema( @@ -1004,7 +1064,7 @@ async def async_step_instruct_unplug( if user_input is not None: if self.usb_path: # USB discovery was used, so the device is already known. - await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) + self._addon_config_updates[CONF_ADDON_DEVICE] = self.usb_path return await self.async_step_start_addon() # Now that the old controller is gone, we can scan for serial ports again return await self.async_step_choose_serial_port() @@ -1136,6 +1196,8 @@ async def async_step_configure_addon_reconfigure( CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } + addon_config_updates = self._addon_config_updates | addon_config_updates + self._addon_config_updates = {} await self._async_set_addon_config(addon_config_updates) if addon_info.state == AddonState.RUNNING and not self.restart_addon: @@ -1207,7 +1269,7 @@ async def async_step_choose_serial_port( """Choose a serial port.""" if user_input is not None: self.usb_path = user_input[CONF_USB_PATH] - await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) + self._addon_config_updates[CONF_ADDON_DEVICE] = self.usb_path return await self.async_step_start_addon() try: diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 0ff635578ea32..fffcb2ca9dd4f 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -113,6 +113,16 @@ "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]" }, + "rf_region": { + "title": "Z-Wave region", + "description": "Select the RF region for your Z-Wave network.", + "data": { + "rf_region": "RF region" + }, + "data_description": { + "rf_region": "The radio frequency region for your Z-Wave network. This must match the region of your Z-Wave devices." + } + }, "start_addon": { "title": "Configuring add-on" }, diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 52b840fb69079..bab13666a2902 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -198,6 +198,17 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]: client.driver.controller.data["sdkVersion"] = original_sdk_version +@pytest.fixture(name="set_country", autouse=True) +def set_country_fixture(hass: HomeAssistant) -> Generator[None]: + """Set the country for the test.""" + original_country = hass.config.country + # Set a default country to avoid asking the user to select it. + hass.config.country = "US" + yield + # Reset the country after the test. + hass.config.country = original_country + + async def test_manual(hass: HomeAssistant) -> None: """Test we create an entry with manual step.""" @@ -4601,3 +4612,324 @@ async def test_recommended_usb_discovery( } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info", "unload_entry") +async def test_addon_rf_region_new_network( + hass: HomeAssistant, + setup_entry: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test RF region selection for new network when country is None.""" + device = "/test" + hass.config.country = None + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "usb_path": device, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "rf_region" + + # Check that all expected RF regions are available + + data_schema = result["data_schema"] + assert data_schema is not None + schema = data_schema.schema + rf_region_field = schema["rf_region"] + selector_options = rf_region_field.config["options"] + + expected_regions = [ + "Australia/New Zealand", + "China", + "Europe", + "Hong Kong", + "India", + "Israel", + "Japan", + "Korea", + "Russia", + "USA", + ] + + assert selector_options == expected_regions + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"rf_region": "Europe"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + # Verify RF region was set in addon config + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": device, + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + "rf_region": "Europe", + } + ), + ) + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert start_addon.call_count == 1 + assert start_addon.call_args == call("core_zwave_js") + assert setup_entry.call_count == 1 + + # avoid unload entry in teardown + entry = result["result"] + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +@pytest.mark.usefixtures("supervisor", "addon_running") +async def test_addon_rf_region_migrate_network( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, + restart_addon: AsyncMock, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + get_server_version: AsyncMock, +) -> None: + """Test migration flow with add-on.""" + hass.config.country = None + version_info = get_server_version.return_value + entry = integration + assert client.connect.call_count == 1 + assert client.driver.controller.home_id == 3245146787 + assert entry.unique_id == "3245146787" + hass.config_entries.async_update_entry( + entry, + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + addon_options["device"] = "/dev/ttyUSB0" + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.controller.data["homeId"] = 3245146787 + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes") as mock_file: + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "choose_serial_port" + data_schema = result["data_schema"] + assert data_schema is not None + assert data_schema.schema[CONF_USB_PATH] + # Ensure the old usb path is not in the list of options + with pytest.raises(InInvalid): + data_schema.schema[CONF_USB_PATH](addon_options["device"]) + + version_info.home_id = 5678 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "rf_region" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"rf_region": "Europe"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": "/test", + "rf_region": "Europe", + } + ), + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert entry.unique_id == "5678" + version_info.home_id = 3245146787 + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 4 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == "3245146787" + assert client.driver.controller.home_id == 3245146787 + + +@pytest.mark.usefixtures("supervisor", "addon_installed", "unload_entry") +@pytest.mark.parametrize(("country", "rf_region"), [("US", "Automatic"), (None, "USA")]) +async def test_addon_skip_rf_region( + hass: HomeAssistant, + setup_entry: AsyncMock, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + start_addon: AsyncMock, + country: str | None, + rf_region: str, +) -> None: + """Test RF region selection is skipped if not needed.""" + device = "/test" + addon_options["rf_region"] = rf_region + hass.config.country = country + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "usb_path": device, + }, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + # Verify RF region was set in addon config + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": device, + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + "rf_region": rf_region, + } + ), + ) + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert start_addon.call_count == 1 + assert start_addon.call_args == call("core_zwave_js") + assert setup_entry.call_count == 1 + + # avoid unload entry in teardown + entry = result["result"] + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED From edc1989ff6aa46f3dfe337132127c47cb618c3a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Aug 2025 08:47:14 -0500 Subject: [PATCH 227/247] Bump onvif-zeep-async to 4.0.4 (#150969) --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 787040d5691db..7ebe5256010ef 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==4.0.3", "WSDiscovery==2.1.2"] + "requirements": ["onvif-zeep-async==4.0.4", "WSDiscovery==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index cce08a7e363d9..1c6a80b1b731d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1594,7 +1594,7 @@ ondilo==0.5.0 onedrive-personal-sdk==0.0.14 # homeassistant.components.onvif -onvif-zeep-async==4.0.3 +onvif-zeep-async==4.0.4 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0d2b71fed83b..8b3fc149d1427 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1362,7 +1362,7 @@ ondilo==0.5.0 onedrive-personal-sdk==0.0.14 # homeassistant.components.onvif -onvif-zeep-async==4.0.3 +onvif-zeep-async==4.0.4 # homeassistant.components.opengarage open-garage==0.2.0 From 71b2d46afd847b02bbf144441c859f15f62f7aea Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 21 Aug 2025 10:33:37 +0200 Subject: [PATCH 228/247] Except ujson from license check (#150980) --- .github/workflows/ci.yaml | 2 +- script/licenses.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2dfd326ec8f09..54522e61ec477 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 5 + CACHE_VERSION: 6 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.8" diff --git a/script/licenses.py b/script/licenses.py index d7819cba5362f..ef62d4970dd1c 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -202,6 +202,7 @@ def from_dict(cls, data: PackageMetadata) -> PackageDefinition: "pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6 "sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14 "tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5 + "ujson", # https://github.com/ultrajson/ultrajson/blob/main/LICENSE.txt } # fmt: off From 82f94de0b803cc262f624a0f9758008a905a2552 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 21 Aug 2025 17:10:27 +0200 Subject: [PATCH 229/247] Enable country site autodetection in Alexa Devices (#150989) --- .../components/alexa_devices/__init__.py | 30 ++++++++++++++-- .../components/alexa_devices/config_flow.py | 13 +++---- .../components/alexa_devices/const.py | 19 ++++++++++ .../components/alexa_devices/coordinator.py | 3 +- .../components/alexa_devices/manifest.json | 2 +- .../components/alexa_devices/strings.json | 4 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/alexa_devices/conftest.py | 5 ++- .../snapshots/test_diagnostics.ambr | 1 - .../alexa_devices/test_config_flow.py | 11 ++---- tests/components/alexa_devices/test_init.py | 35 +++++++++++++++++-- 12 files changed, 92 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index 9df0e60850e8e..c08e2f1c01078 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -1,11 +1,11 @@ """Alexa Devices integration.""" -from homeassistant.const import Platform +from homeassistant.const import CONF_COUNTRY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import _LOGGER, COUNTRY_DOMAINS, DOMAIN from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator from .services import async_setup_services @@ -40,6 +40,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo return True +async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: + """Migrate old entry.""" + if entry.version == 1 and entry.minor_version == 0: + _LOGGER.debug( + "Migrating from version %s.%s", entry.version, entry.minor_version + ) + + # Convert country in domain + country = entry.data[CONF_COUNTRY] + domain = COUNTRY_DOMAINS.get(country, country) + + # Save domain and remove country + new_data = entry.data.copy() + new_data.update({"site": f"https://www.amazon.{domain}"}) + + hass.config_entries.async_update_entry( + entry, data=new_data, version=1, minor_version=1 + ) + + _LOGGER.info( + "Migration to version %s.%s successful", entry.version, entry.minor_version + ) + + return True + + async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index 3e705d73adee0..ca00d3e8250ca 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -10,16 +10,14 @@ CannotAuthenticate, CannotConnect, CannotRetrieveData, - WrongCountry, ) import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.selector import CountrySelector from .const import CONF_LOGIN_DATA, DOMAIN @@ -37,7 +35,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, session = aiohttp_client.async_create_clientsession(hass) api = AmazonEchoApi( session, - data[CONF_COUNTRY], data[CONF_USERNAME], data[CONF_PASSWORD], ) @@ -48,6 +45,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Alexa Devices.""" + VERSION = 1 + MINOR_VERSION = 1 + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -62,8 +62,6 @@ async def async_step_user( errors["base"] = "invalid_auth" except CannotRetrieveData: errors["base"] = "cannot_retrieve_data" - except WrongCountry: - errors["base"] = "wrong_country" else: await self.async_set_unique_id(data["customer_info"]["user_id"]) self._abort_if_unique_id_configured() @@ -78,9 +76,6 @@ async def async_step_user( errors=errors, data_schema=vol.Schema( { - vol.Required( - CONF_COUNTRY, default=self.hass.config.country - ): CountrySelector(), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_CODE): cv.string, diff --git a/homeassistant/components/alexa_devices/const.py b/homeassistant/components/alexa_devices/const.py index ca0290a10bc79..3ade3ad3ecdf8 100644 --- a/homeassistant/components/alexa_devices/const.py +++ b/homeassistant/components/alexa_devices/const.py @@ -6,3 +6,22 @@ DOMAIN = "alexa_devices" CONF_LOGIN_DATA = "login_data" + +DEFAULT_DOMAIN = {"domain": "com"} +COUNTRY_DOMAINS = { + "ar": DEFAULT_DOMAIN, + "at": DEFAULT_DOMAIN, + "au": {"domain": "com.au"}, + "be": {"domain": "com.be"}, + "br": DEFAULT_DOMAIN, + "gb": {"domain": "co.uk"}, + "il": DEFAULT_DOMAIN, + "jp": {"domain": "co.jp"}, + "mx": {"domain": "com.mx"}, + "no": DEFAULT_DOMAIN, + "nz": {"domain": "com.au"}, + "pl": DEFAULT_DOMAIN, + "tr": {"domain": "com.tr"}, + "us": DEFAULT_DOMAIN, + "za": {"domain": "co.za"}, +} diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index f4a1faa4f81bb..ac033a487ee3f 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -11,7 +11,7 @@ from aiohttp import ClientSession from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -44,7 +44,6 @@ def __init__( ) self.api = AmazonEchoApi( session, - entry.data[CONF_COUNTRY], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.data[CONF_LOGIN_DATA], diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 90410412dfa10..cba3af83f44d6 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "silver", - "requirements": ["aioamazondevices==4.0.0"] + "requirements": ["aioamazondevices==5.0.0"] } diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index 1b1150d564975..720b357d275dd 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -1,7 +1,6 @@ { "common": { "data_code": "One-time password (OTP code)", - "data_description_country": "The country where your Amazon account is registered.", "data_description_username": "The email address of your Amazon account.", "data_description_password": "The password of your Amazon account.", "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.", @@ -12,13 +11,11 @@ "step": { "user": { "data": { - "country": "[%key:common::config_flow::data::country%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "code": "[%key:component::alexa_devices::common::data_code%]" }, "data_description": { - "country": "[%key:component::alexa_devices::common::data_description_country%]", "username": "[%key:component::alexa_devices::common::data_description_username%]", "password": "[%key:component::alexa_devices::common::data_description_password%]", "code": "[%key:component::alexa_devices::common::data_description_code%]" @@ -46,7 +43,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.", "unknown": "[%key:common::config_flow::error::unknown%]" } }, diff --git a/requirements_all.txt b/requirements_all.txt index 1c6a80b1b731d..e89fe196fc61d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==4.0.0 +aioamazondevices==5.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b3fc149d1427..cd45aacc4b1b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==4.0.0 +aioamazondevices==5.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index 2259670686221..3c68b7b76261c 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -8,9 +8,9 @@ import pytest from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN -from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME +from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME from tests.common import MockConfigEntry @@ -80,7 +80,6 @@ def mock_config_entry() -> MockConfigEntry: domain=DOMAIN, title="Amazon Test Account", data={ - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: {"session": "test-session"}, diff --git a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr index 95798fca817db..0f3c3647e90a8 100644 --- a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr +++ b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr @@ -47,7 +47,6 @@ }), 'entry': dict({ 'data': dict({ - 'country': 'IT', 'login_data': dict({ 'session': 'test-session', }), diff --git a/tests/components/alexa_devices/test_config_flow.py b/tests/components/alexa_devices/test_config_flow.py index e1b2974184b8c..e4b0f8aa08733 100644 --- a/tests/components/alexa_devices/test_config_flow.py +++ b/tests/components/alexa_devices/test_config_flow.py @@ -6,17 +6,16 @@ CannotAuthenticate, CannotConnect, CannotRetrieveData, - WrongCountry, ) import pytest from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME +from .const import TEST_CODE, TEST_PASSWORD, TEST_USERNAME from tests.common import MockConfigEntry @@ -37,7 +36,6 @@ async def test_full_flow( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -46,7 +44,6 @@ async def test_full_flow( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: { @@ -63,7 +60,6 @@ async def test_full_flow( (CannotConnect, "cannot_connect"), (CannotAuthenticate, "invalid_auth"), (CannotRetrieveData, "cannot_retrieve_data"), - (WrongCountry, "wrong_country"), ], ) async def test_flow_errors( @@ -87,7 +83,6 @@ async def test_flow_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -102,7 +97,6 @@ async def test_flow_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -131,7 +125,6 @@ async def test_already_configured( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, diff --git a/tests/components/alexa_devices/test_init.py b/tests/components/alexa_devices/test_init.py index 3100cfe5fa934..c628a5e00e7b1 100644 --- a/tests/components/alexa_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -4,12 +4,14 @@ from syrupy.assertion import SnapshotAssertion -from homeassistant.components.alexa_devices.const import DOMAIN +from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from . import setup_integration -from .const import TEST_SERIAL_NUMBER +from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME from tests.common import MockConfigEntry @@ -28,3 +30,32 @@ async def test_device_info( ) assert device_entry is not None assert device_entry == snapshot + + +async def test_migrate_entry( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful migration of entry data.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Amazon Test Account", + data={ + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_LOGIN_DATA: {"session": "test-session"}, + }, + unique_id=TEST_USERNAME, + version=1, + minor_version=0, + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.minor_version == 1 + assert config_entry.data["site"] == f"https://www.amazon.{TEST_COUNTRY}" From 61a50e77cfbccca0797abac7345599b922e9dbbe Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 21 Aug 2025 17:02:02 +0200 Subject: [PATCH 230/247] Update frontend to 20250811.1 (#151005) --- 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 3488ddc5e5cb1..9fc80cf0e8a5c 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==20250811.0"] + "requirements": ["home-assistant-frontend==20250811.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e04f73bc42534..834e04abbf07b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==4.0.2 hass-nabucasa==0.111.2 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250811.0 +home-assistant-frontend==20250811.1 home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e89fe196fc61d..bef2cc417b3c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250811.0 +home-assistant-frontend==20250811.1 # homeassistant.components.conversation home-assistant-intents==2025.7.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd45aacc4b1b6..e005f5f7764f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250811.0 +home-assistant-frontend==20250811.1 # homeassistant.components.conversation home-assistant-intents==2025.7.30 From bb4f8adffe7f59f10ead9a2ea3ca651c9b4d95a9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Aug 2025 15:13:51 +0000 Subject: [PATCH 231/247] Bump version to 2025.8.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 9ddbac360afff..5058f988958c2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -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, 2) diff --git a/pyproject.toml b/pyproject.toml index ced768ae63ea7..c819973959237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.2" +version = "2025.8.3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From c2c561bc21159ea2d22fd1c4dc72d42ae83fa263 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Wed, 27 Aug 2025 07:43:53 +0200 Subject: [PATCH 232/247] Don't use custom bypass in SIA (#132628) --- homeassistant/components/sia/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index bb6a0669a9976..a3bed65287683 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -47,7 +47,7 @@ class SIAAlarmControlPanelEntityDescription( "CP": AlarmControlPanelState.ARMED_AWAY, "CQ": AlarmControlPanelState.ARMED_AWAY, "CS": AlarmControlPanelState.ARMED_AWAY, - "CF": AlarmControlPanelState.ARMED_CUSTOM_BYPASS, + "CF": AlarmControlPanelState.ARMED_AWAY, "NP": AlarmControlPanelState.DISARMED, "NO": AlarmControlPanelState.DISARMED, "OA": AlarmControlPanelState.DISARMED, From d4bc066cc46d7423538721a7d42e2a9a86c72ebb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Aug 2025 07:46:02 +0200 Subject: [PATCH 233/247] Bump bleak-retry-connector to 4.4.1 (#151217) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9efbd321123f1..d29a2cd417ae3 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "quality_scale": "internal", "requirements": [ "bleak==1.0.1", - "bleak-retry-connector==4.3.0", + "bleak-retry-connector==4.4.1", "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 12a59a97903ca..70f121d8c98eb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ audioop-lts==0.2.1 av==13.1.0 awesomeversion==25.5.0 bcrypt==4.3.0 -bleak-retry-connector==4.3.0 +bleak-retry-connector==4.4.1 bleak==1.0.1 bluetooth-adapters==2.0.0 bluetooth-auto-recovery==1.5.2 diff --git a/requirements_all.txt b/requirements_all.txt index 14c6d29ecca71..79d05cf2672d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,7 +628,7 @@ bizkaibus==0.1.1 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.3.0 +bleak-retry-connector==4.4.1 # homeassistant.components.bluetooth bleak==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46a7cba3f5aa5..ed4d55e95c7e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ bimmer-connected[china]==0.17.2 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.3.0 +bleak-retry-connector==4.4.1 # homeassistant.components.bluetooth bleak==1.0.1 From d72cc45ca84e6570f8e5163fffdaa8dca31e6f3c Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 27 Aug 2025 07:46:21 +0200 Subject: [PATCH 234/247] Bump aioautomower to 2.2.0 (#151207) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 49eb364858fa9..60ac9fe4fa553 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["aioautomower"], "quality_scale": "silver", - "requirements": ["aioautomower==2.1.2"] + "requirements": ["aioautomower==2.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 79d05cf2672d7..8d732aac02947 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2.1.2 +aioautomower==2.2.0 # homeassistant.components.azure_devops aioazuredevops==2.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed4d55e95c7e8..bc00dc8fba41a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -192,7 +192,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2.1.2 +aioautomower==2.2.0 # homeassistant.components.azure_devops aioazuredevops==2.2.2 From 0bb16befbd5de7f934090d2d087a3181c2dcae3a Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 27 Aug 2025 07:47:14 +0200 Subject: [PATCH 235/247] Make event entity dependend on websocket in Husqvarna Automower (#151203) --- .../husqvarna_automower/coordinator.py | 10 +++++++-- .../components/husqvarna_automower/event.py | 18 +++++++++++++++ .../husqvarna_automower/conftest.py | 22 +++++++++++++++++-- .../husqvarna_automower/test_event.py | 22 ++++++++++++++++++- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 91c1e619d0b07..9932aaacb6559 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -62,6 +62,7 @@ def __init__( self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = [] self.pong: datetime | None = None self.websocket_alive: bool = False + self.websocket_callbacks: list[Callable[[bool], None]] = [] self._watchdog_task: asyncio.Task | None = None @override @@ -198,12 +199,17 @@ def _should_poll(self) -> bool: ) async def _pong_watchdog(self) -> None: + """Watchdog to check for pong messages.""" _LOGGER.debug("Watchdog started") try: while True: _LOGGER.debug("Sending ping") - self.websocket_alive = await self.api.send_empty_message() - _LOGGER.debug("Ping result: %s", self.websocket_alive) + is_alive = await self.api.send_empty_message() + _LOGGER.debug("Ping result: %s", is_alive) + if self.websocket_alive != is_alive: + self.websocket_alive = is_alive + for ws_callback in self.websocket_callbacks: + ws_callback(is_alive) await asyncio.sleep(PING_INTERVAL) _LOGGER.debug("Websocket alive %s", self.websocket_alive) diff --git a/homeassistant/components/husqvarna_automower/event.py b/homeassistant/components/husqvarna_automower/event.py index 8e2e48b940d5d..7fe8bae8c2de6 100644 --- a/homeassistant/components/husqvarna_automower/event.py +++ b/homeassistant/components/husqvarna_automower/event.py @@ -1,6 +1,7 @@ """Creates the event entities for supported mowers.""" from collections.abc import Callable +import logging from aioautomower.model import SingleMessageData @@ -18,6 +19,7 @@ from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerBaseEntity +_LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 ATTR_SEVERITY = "severity" @@ -80,6 +82,12 @@ def __init__( """Initialize Automower message event entity.""" super().__init__(mower_id, coordinator) self._attr_unique_id = f"{mower_id}_message" + self.websocket_alive: bool = coordinator.websocket_alive + + @property + def available(self) -> bool: + """Return True if the entity is available.""" + return self.websocket_alive and self.mower_id in self.coordinator.data @callback def _handle(self, msg: SingleMessageData) -> None: @@ -102,7 +110,17 @@ async def async_added_to_hass(self) -> None: """Register callback when entity is added to hass.""" await super().async_added_to_hass() self.coordinator.api.register_single_message_callback(self._handle) + self.coordinator.websocket_callbacks.append(self._handle_websocket_update) async def async_will_remove_from_hass(self) -> None: """Unregister WebSocket callback when entity is removed.""" self.coordinator.api.unregister_single_message_callback(self._handle) + self.coordinator.websocket_callbacks.remove(self._handle_websocket_update) + + def _handle_websocket_update(self, is_alive: bool) -> None: + """Handle websocket status changes.""" + if self.websocket_alive == is_alive: + return + self.websocket_alive = is_alive + _LOGGER.debug("WebSocket status changed to %s, updating entity state", is_alive) + self.async_write_ha_state() diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 1cd6f9b393ec8..02b9b2715a1ea 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -1,7 +1,7 @@ """Test helpers for Husqvarna Automower.""" import asyncio -from collections.abc import Generator +from collections.abc import Callable, Generator import time from unittest.mock import AsyncMock, create_autospec, patch @@ -16,7 +16,7 @@ async_import_client_credential, ) from homeassistant.components.husqvarna_automower.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -137,3 +137,21 @@ async def listen() -> None: spec_set=True, ) yield mock_instance + + +@pytest.fixture +def automower_ws_ready(mock_automower_client: AsyncMock) -> list[Callable[[], None]]: + """Fixture to capture ws_ready_callbacks.""" + + ws_ready_callbacks: list[Callable[[], None]] = [] + + @callback + def fake_register_ws_ready_callback(cb: Callable[[], None]) -> None: + ws_ready_callbacks.append(cb) + + mock_automower_client.register_ws_ready_callback.side_effect = ( + fake_register_ws_ready_callback + ) + mock_automower_client.send_empty_message.return_value = True + + return ws_ready_callbacks diff --git a/tests/components/husqvarna_automower/test_event.py b/tests/components/husqvarna_automower/test_event.py index 6cbfa10297607..c4121c1cfb87d 100644 --- a/tests/components/husqvarna_automower/test_event.py +++ b/tests/components/husqvarna_automower/test_event.py @@ -33,6 +33,7 @@ async def test_event( mock_config_entry: MockConfigEntry, freezer: FrozenDateTimeFactory, values: dict[str, MowerAttributes], + automower_ws_ready: list[Callable[[], None]], ) -> None: """Test that a new message arriving over the websocket creates and updates the sensor.""" callbacks: list[Callable[[SingleMessageData], None]] = [] @@ -46,11 +47,17 @@ def fake_register_websocket_response( mock_automower_client.register_single_message_callback.side_effect = ( fake_register_websocket_response ) + mock_automower_client.send_empty_message.return_value = True # Set up integration await setup_integration(hass, mock_config_entry) await hass.async_block_till_done() + # Start the watchdog and let it run once to set websocket_alive=True + for cb in automower_ws_ready: + cb() + await hass.async_block_till_done() + # Ensure callback was registered for the test mower assert mock_automower_client.register_single_message_callback.called @@ -76,6 +83,7 @@ def fake_register_websocket_response( for cb in callbacks: cb(message) await hass.async_block_till_done() + state = hass.states.get("event.test_mower_1_message") assert state is not None assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left" @@ -84,6 +92,12 @@ def fake_register_websocket_response( await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED + + # Start the new watchdog and let it run + for cb in automower_ws_ready: + cb() + await hass.async_block_till_done() + state = hass.states.get("event.test_mower_1_message") assert state is not None assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left" @@ -129,6 +143,7 @@ def fake_register_websocket_response( for cb in callbacks: cb(message) await hass.async_block_till_done() + entry = entity_registry.async_get("event.test_mower_1_message") assert entry is not None assert state.attributes[ATTR_EVENT_TYPE] == "alarm_mower_lifted" @@ -154,9 +169,9 @@ async def test_event_snapshot( hass: HomeAssistant, mock_automower_client: AsyncMock, mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + automower_ws_ready: list[Callable[[], None]], ) -> None: """Test that a new message arriving over the websocket updates the sensor.""" with patch( @@ -179,6 +194,11 @@ def fake_register_websocket_response( await setup_integration(hass, mock_config_entry) await hass.async_block_till_done() + # Start the watchdog and let it run once + for cb in automower_ws_ready: + cb() + await hass.async_block_till_done() + # Ensure callback was registered for the test mower assert mock_automower_client.register_single_message_callback.called From 50a2eba66e78095a0c6a02b92a3c95b419eb9d06 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Wed, 27 Aug 2025 02:14:42 -0400 Subject: [PATCH 236/247] Add platform patching in `init_integration` fixture in copilot-instructions.md (#151173) --- .github/copilot-instructions.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7eba0203f7e69..fc6f4a537242e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1073,7 +1073,11 @@ async def test_flow_connection_error(hass, mock_api_error): ### Entity Testing Patterns ```python -@pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True) +@pytest.fixture +def platforms() -> list[Platform]: + """Overridden fixture to specify platforms to test.""" + return [Platform.SENSOR] # Or another specific platform as needed. + @pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration") async def test_entities( hass: HomeAssistant, @@ -1120,16 +1124,25 @@ def mock_device_api() -> Generator[MagicMock]: ) yield api +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to specify platforms to test.""" + return PLATFORMS + @pytest.fixture async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_device_api: MagicMock, + platforms: list[Platform], ) -> MockConfigEntry: """Set up the integration for testing.""" mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + + with patch("homeassistant.components.my_integration.PLATFORMS", platforms): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + return mock_config_entry ``` From 6e45713d3a0323b1f33a922b0cf3abab33ac172f Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Wed, 27 Aug 2025 17:49:05 +1000 Subject: [PATCH 237/247] Ask for PIN in Husqvarna Automower BLE integration (#135440) Signed-off-by: Alistair Francis Co-authored-by: Erik Montnemery Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com> --- .../husqvarna_automower_ble/__init__.py | 20 +- .../husqvarna_automower_ble/config_flow.py | 266 +++++++++++-- .../husqvarna_automower_ble/strings.json | 37 +- .../husqvarna_automower_ble/conftest.py | 3 +- .../test_config_flow.py | 371 ++++++++++++++++-- .../husqvarna_automower_ble/test_init.py | 49 ++- 6 files changed, 668 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/husqvarna_automower_ble/__init__.py b/homeassistant/components/husqvarna_automower_ble/__init__.py index 4537dec0e287d..89de333644038 100644 --- a/homeassistant/components/husqvarna_automower_ble/__init__.py +++ b/homeassistant/components/husqvarna_automower_ble/__init__.py @@ -9,11 +9,11 @@ from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, Platform +from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from .const import LOGGER +from .const import DOMAIN, LOGGER from .coordinator import HusqvarnaCoordinator type HusqvarnaConfigEntry = ConfigEntry[HusqvarnaCoordinator] @@ -26,10 +26,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: HusqvarnaConfigEntry) -> bool: """Set up Husqvarna Autoconnect Bluetooth from a config entry.""" + if CONF_PIN not in entry.data: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="pin_required", + translation_placeholders={"domain_name": "Husqvarna Automower BLE"}, + ) + address = entry.data[CONF_ADDRESS] + pin = int(entry.data[CONF_PIN]) channel_id = entry.data[CONF_CLIENT_ID] - mower = Mower(channel_id, address) + mower = Mower(channel_id, address, pin) await close_stale_connections_by_address(address) @@ -39,6 +47,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: HusqvarnaConfigEntry) -> hass, address, connectable=True ) or await get_device(address) response_result = await mower.connect(device) + if response_result == ResponseResult.INVALID_PIN: + raise ConfigEntryAuthFailed( + f"Unable to connect to device {address} due to wrong PIN" + ) if response_result != ResponseResult.OK: raise ConfigEntryNotReady( f"Unable to connect to device {address}, mower returned {response_result}" diff --git a/homeassistant/components/husqvarna_automower_ble/config_flow.py b/homeassistant/components/husqvarna_automower_ble/config_flow.py index 72835c223341d..15de6bde70825 100644 --- a/homeassistant/components/husqvarna_automower_ble/config_flow.py +++ b/homeassistant/components/husqvarna_automower_ble/config_flow.py @@ -2,17 +2,20 @@ from __future__ import annotations +from collections.abc import Mapping import random from typing import Any from automower_ble.mower import Mower +from automower_ble.protocol import ResponseResult from bleak import BleakError +from bleak_retry_connector import get_device import voluptuous as vol from homeassistant.components import bluetooth from homeassistant.components.bluetooth import BluetoothServiceInfo -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID +from homeassistant.config_entries import SOURCE_BLUETOOTH, ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN from .const import DOMAIN, LOGGER @@ -39,14 +42,23 @@ def _is_supported(discovery_info: BluetoothServiceInfo): return manufacturer and service_husqvarna and service_generic +def _pin_valid(pin: str) -> bool: + """Check if the pin is valid.""" + try: + int(pin) + except (TypeError, ValueError): + return False + return True + + class HusqvarnaAutomowerBleConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Husqvarna Bluetooth.""" VERSION = 1 - def __init__(self) -> None: - """Initialize the config flow.""" - self.address: str | None + address: str | None = None + mower_name: str = "" + pin: str | None = None async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo @@ -60,62 +72,244 @@ async def async_step_bluetooth( self.address = discovery_info.address await self.async_set_unique_id(self.address) self._abort_if_unique_id_configured() - return await self.async_step_confirm() + return await self.async_step_bluetooth_confirm() - async def async_step_confirm( + async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Confirm discovery.""" + """Confirm Bluetooth discovery.""" assert self.address + errors: dict[str, str] = {} - device = bluetooth.async_ble_device_from_address( - self.hass, self.address, connectable=True + if user_input is not None: + if not _pin_valid(user_input[CONF_PIN]): + errors["base"] = "invalid_pin" + else: + self.pin = user_input[CONF_PIN] + return await self.check_mower(user_input) + + return self.async_show_form( + step_id="bluetooth_confirm", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_PIN): str, + }, + ), + user_input, + ), + description_placeholders={"name": self.mower_name or self.address}, + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial manual step.""" + errors: dict[str, str] = {} + + if user_input is not None: + if not _pin_valid(user_input[CONF_PIN]): + errors["base"] = "invalid_pin" + else: + self.address = user_input[CONF_ADDRESS] + self.pin = user_input[CONF_PIN] + await self.async_set_unique_id(self.address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return await self.check_mower(user_input) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_ADDRESS): str, + vol.Required(CONF_PIN): str, + }, + ), + user_input, + ), + errors=errors, ) + + async def probe_mower(self, device) -> str | None: + """Probe the mower to see if it exists.""" channel_id = random.randint(1, 0xFFFFFFFF) + assert self.address + try: (manufacturer, device_type, model) = await Mower( channel_id, self.address ).probe_gatts(device) except (BleakError, TimeoutError) as exception: - LOGGER.exception("Failed to connect to device: %s", exception) - return self.async_abort(reason="cannot_connect") + LOGGER.exception("Failed to probe device (%s): %s", self.address, exception) + return None title = manufacturer + " " + device_type LOGGER.debug("Found device: %s", title) - if user_input is not None: - return self.async_create_entry( - title=title, - data={CONF_ADDRESS: self.address, CONF_CLIENT_ID: channel_id}, - ) + return title - self.context["title_placeholders"] = { - "name": title, - } + async def connect_mower(self, device) -> tuple[int, Mower]: + """Connect to the Mower.""" + assert self.address + assert self.pin is not None - self._set_confirm_only() - return self.async_show_form( - step_id="confirm", - description_placeholders=self.context["title_placeholders"], + channel_id = random.randint(1, 0xFFFFFFFF) + mower = Mower(channel_id, self.address, int(self.pin)) + + return (channel_id, mower) + + async def check_mower( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: + """Check that the mower exists and is setup.""" + assert self.address + + device = bluetooth.async_ble_device_from_address( + self.hass, self.address, connectable=True ) - async def async_step_user( + title = await self.probe_mower(device) + if title is None: + return self.async_abort(reason="cannot_connect") + self.mower_name = title + + try: + errors: dict[str, str] = {} + + (channel_id, mower) = await self.connect_mower(device) + + response_result = await mower.connect(device) + await mower.disconnect() + + if response_result is not ResponseResult.OK: + LOGGER.debug("cannot connect, response: %s", response_result) + + if ( + response_result is ResponseResult.INVALID_PIN + or response_result is ResponseResult.NOT_ALLOWED + ): + errors["base"] = "invalid_auth" + else: + errors["base"] = "cannot_connect" + + if self.source == SOURCE_BLUETOOTH: + return self.async_show_form( + step_id="bluetooth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PIN): str, + }, + ), + description_placeholders={ + "name": self.mower_name or self.address + }, + errors=errors, + ) + + suggested_values = {} + + if self.address: + suggested_values[CONF_ADDRESS] = self.address + if self.pin: + suggested_values[CONF_PIN] = self.pin + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_ADDRESS): str, + vol.Required(CONF_PIN): str, + }, + ), + suggested_values, + ), + errors=errors, + ) + except (TimeoutError, BleakError): + return self.async_abort(reason="cannot_connect") + + return self.async_create_entry( + title=title, + data={ + CONF_ADDRESS: self.address, + CONF_CLIENT_ID: channel_id, + CONF_PIN: self.pin, + }, + ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauthentication upon an API authentication error.""" + reauth_entry = self._get_reauth_entry() + self.address = reauth_entry.data[CONF_ADDRESS] + self.mower_name = reauth_entry.title + self.pin = reauth_entry.data.get(CONF_PIN, "") + + self.context["title_placeholders"] = { + "name": self.mower_name, + "address": self.address, + } + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle the initial step.""" - if user_input is not None: - self.address = user_input[CONF_ADDRESS] - await self.async_set_unique_id(self.address, raise_on_progress=False) - self._abort_if_unique_id_configured() - return await self.async_step_confirm() + """Confirm reauthentication dialog.""" + errors: dict[str, str] = {} + + if user_input is not None and not _pin_valid(user_input[CONF_PIN]): + errors["base"] = "invalid_pin" + elif user_input is not None: + reauth_entry = self._get_reauth_entry() + self.pin = user_input[CONF_PIN] + + try: + assert self.address + device = bluetooth.async_ble_device_from_address( + self.hass, self.address, connectable=True + ) or await get_device(self.address) + + mower = Mower( + reauth_entry.data[CONF_CLIENT_ID], self.address, int(self.pin) + ) + + response_result = await mower.connect(device) + await mower.disconnect() + if ( + response_result is ResponseResult.INVALID_PIN + or response_result is ResponseResult.NOT_ALLOWED + ): + errors["base"] = "invalid_auth" + elif response_result is not ResponseResult.OK: + errors["base"] = "cannot_connect" + else: + return self.async_update_reload_and_abort( + self._get_reauth_entry(), + data=reauth_entry.data | {CONF_PIN: self.pin}, + ) + + except (TimeoutError, BleakError): + # We don't want to abort a reauth flow when we can't connect, so + # we just show the form again with an error. + errors["base"] = "cannot_connect" return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_ADDRESS): str, - }, + step_id="reauth_confirm", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_PIN): str, + }, + ), + {CONF_PIN: self.pin}, ), + description_placeholders={"name": self.mower_name}, + errors=errors, ) diff --git a/homeassistant/components/husqvarna_automower_ble/strings.json b/homeassistant/components/husqvarna_automower_ble/strings.json index de0a140933acd..64ae632330cee 100644 --- a/homeassistant/components/husqvarna_automower_ble/strings.json +++ b/homeassistant/components/husqvarna_automower_ble/strings.json @@ -4,18 +4,49 @@ "step": { "user": { "data": { - "address": "Device BLE address" + "address": "Device BLE address", + "pin": "Mower PIN" + }, + "data_description": { + "pin": "The PIN used to secure the mower" } }, - "confirm": { - "description": "Do you want to set up {name}? Make sure the mower is in pairing mode" + "bluetooth_confirm": { + "description": "Do you want to set up {name}?\nMake sure the mower is in pairing mode.", + "data": { + "pin": "[%key:component::husqvarna_automower_ble::config::step::user::data::pin%]" + }, + "data_description": { + "pin": "[%key:component::husqvarna_automower_ble::config::step::user::data_description::pin%]" + } + }, + "reauth_confirm": { + "description": "Please confirm the PIN for {name}.", + "data": { + "pin": "[%key:component::husqvarna_automower_ble::config::step::user::data::pin%]" + }, + "data_description": { + "pin": "[%key:component::husqvarna_automower_ble::config::step::user::data_description::pin%]" + } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "Ensure the mower is in pairing mode and try again. It can take a few attempts.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "not_allowed": "Unable to read data from the mower, this usually means it is not paired", "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "Unable to pair with device, ensure the PIN is correct and the mower is in pairing mode", + "invalid_pin": "The PIN must be a number" + } + }, + "exceptions": { + "pin_required": { + "message": "PIN is required for {domain_name}" } } } diff --git a/tests/components/husqvarna_automower_ble/conftest.py b/tests/components/husqvarna_automower_ble/conftest.py index 1081db014e3fe..820edb29059ab 100644 --- a/tests/components/husqvarna_automower_ble/conftest.py +++ b/tests/components/husqvarna_automower_ble/conftest.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.husqvarna_automower_ble.const import DOMAIN -from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID +from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN from . import AUTOMOWER_SERVICE_INFO @@ -58,6 +58,7 @@ def mock_config_entry() -> MockConfigEntry: data={ CONF_ADDRESS: AUTOMOWER_SERVICE_INFO.address, CONF_CLIENT_ID: 1197489078, + CONF_PIN: "1234", }, unique_id=AUTOMOWER_SERVICE_INFO.address, ) diff --git a/tests/components/husqvarna_automower_ble/test_config_flow.py b/tests/components/husqvarna_automower_ble/test_config_flow.py index e053a28b7dd47..41dfdffae7388 100644 --- a/tests/components/husqvarna_automower_ble/test_config_flow.py +++ b/tests/components/husqvarna_automower_ble/test_config_flow.py @@ -2,12 +2,13 @@ from unittest.mock import Mock, patch +from automower_ble.protocol import ResponseResult from bleak import BleakError import pytest from homeassistant.components.husqvarna_automower_ble.const import DOMAIN from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER -from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID +from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -36,8 +37,6 @@ def mock_random() -> Mock: async def test_user_selection(hass: HomeAssistant) -> None: """Test we can select a device.""" - inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO) - inject_bluetooth_service_info(hass, AUTOMOWER_UNNAMED_SERVICE_INFO) await hass.async_block_till_done(wait_background_tasks=True) result = await hass.config_entries.flow.async_init( @@ -48,22 +47,77 @@ async def test_user_selection(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000001"}, + user_input={ + CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", + CONF_PIN: "1234", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Husqvarna Automower" + assert result["result"].unique_id == "00000000-0000-0000-0000-000000000001" + + assert result["data"] == { + CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", + CONF_CLIENT_ID: 1197489078, + CONF_PIN: "1234", + } + + +async def test_user_selection_incorrect_pin( + hass: HomeAssistant, + mock_automower_client: Mock, +) -> None: + """Test we can select a device.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # Try non numeric pin + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", + CONF_PIN: "ABCD", + }, ) + assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_pin"} + # Try wrong PIN + mock_automower_client.connect.return_value = ResponseResult.INVALID_PIN result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={}, + user_input={ + CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", + CONF_PIN: "1234", + }, ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + mock_automower_client.connect.return_value = ResponseResult.OK + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", + CONF_PIN: "1234", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Husqvarna Automower" - assert result["result"].unique_id == "00000000-0000-0000-0000-000000000001" assert result["data"] == { CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", CONF_CLIENT_ID: 1197489078, + CONF_PIN: "1234", } @@ -74,13 +128,13 @@ async def test_bluetooth(hass: HomeAssistant) -> None: await hass.async_block_till_done(wait_background_tasks=True) result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0] - assert result["step_id"] == "confirm" - assert result["context"]["unique_id"] == "00000000-0000-0000-0000-000000000003" + assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={}, + user_input={CONF_PIN: "1234"}, ) + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Husqvarna Automower" assert result["result"].unique_id == "00000000-0000-0000-0000-000000000003" @@ -88,6 +142,135 @@ async def test_bluetooth(hass: HomeAssistant) -> None: assert result["data"] == { CONF_ADDRESS: "00000000-0000-0000-0000-000000000003", CONF_CLIENT_ID: 1197489078, + CONF_PIN: "1234", + } + + +async def test_bluetooth_incorrect_pin( + hass: HomeAssistant, + mock_automower_client: Mock, +) -> None: + """Test we can select a device.""" + + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=AUTOMOWER_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + # Try non numeric pin + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "ABCD", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + assert result["errors"] == {"base": "invalid_pin"} + + # Try wrong PIN + mock_automower_client.connect.return_value = ResponseResult.INVALID_PIN + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "5678"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + mock_automower_client.connect.return_value = ResponseResult.OK + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "1234"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Husqvarna Automower" + assert result["result"].unique_id == "00000000-0000-0000-0000-000000000003" + + assert result["data"] == { + CONF_ADDRESS: "00000000-0000-0000-0000-000000000003", + CONF_CLIENT_ID: 1197489078, + CONF_PIN: "1234", + } + + +async def test_bluetooth_unknown_error( + hass: HomeAssistant, + mock_automower_client: Mock, +) -> None: + """Test we can select a device.""" + + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=AUTOMOWER_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + mock_automower_client.connect.return_value = ResponseResult.UNKNOWN_ERROR + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "5678"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + +async def test_bluetooth_not_paired( + hass: HomeAssistant, + mock_automower_client: Mock, +) -> None: + """Test we can select a device.""" + + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=AUTOMOWER_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + mock_automower_client.connect.return_value = ResponseResult.NOT_ALLOWED + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "5678"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + mock_automower_client.connect.return_value = ResponseResult.OK + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "1234"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Husqvarna Automower" + assert result["result"].unique_id == "00000000-0000-0000-0000-000000000003" + + assert result["data"] == { + CONF_ADDRESS: "00000000-0000-0000-0000-000000000003", + CONF_CLIENT_ID: 1197489078, + CONF_PIN: "1234", } @@ -106,17 +289,90 @@ async def test_bluetooth_invalid(hass: HomeAssistant) -> None: assert result["reason"] == "no_devices_found" -async def test_failed_connect( +async def test_successful_reauth( hass: HomeAssistant, mock_automower_client: Mock, + mock_config_entry: MockConfigEntry, ) -> None: """Test we can select a device.""" - inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO) - inject_bluetooth_service_info(hass, AUTOMOWER_UNNAMED_SERVICE_INFO) + mock_config_entry.add_to_hass(hass) + + await hass.async_block_till_done(wait_background_tasks=True) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + # Try non numeric pin + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "ABCD", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_pin"} + + # Try connection error + mock_automower_client.connect.return_value = ResponseResult.UNKNOWN_ERROR + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "5678", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "cannot_connect"} + + # Try wrong PIN + mock_automower_client.connect.return_value = ResponseResult.INVALID_PIN + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "5678", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + mock_automower_client.connect.return_value = ResponseResult.OK + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "1234", + }, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries("husqvarna_automower_ble")) == 1 + + assert ( + mock_config_entry.data[CONF_ADDRESS] == "00000000-0000-0000-0000-000000000003" + ) + assert mock_config_entry.data[CONF_CLIENT_ID] == 1197489078 + assert mock_config_entry.data[CONF_PIN] == "1234" + + +async def test_user_unable_to_connect( + hass: HomeAssistant, + mock_automower_client: Mock, +) -> None: + """Test we can select a device.""" await hass.async_block_till_done(wait_background_tasks=True) - mock_automower_client.connect.side_effect = False + mock_automower_client.connect.side_effect = BleakError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -126,23 +382,41 @@ async def test_failed_connect( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000001"}, + user_input={ + CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", + CONF_PIN: "1234", + }, ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_failed_reauth( + hass: HomeAssistant, + mock_automower_client: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can select a device.""" + + mock_config_entry.add_to_hass(hass) + + await hass.async_block_till_done(wait_background_tasks=True) + + mock_automower_client.connect.side_effect = BleakError + + result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={}, + user_input={ + CONF_PIN: "5678", + }, ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Husqvarna Automower" - assert result["result"].unique_id == "00000000-0000-0000-0000-000000000001" - - assert result["data"] == { - CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", - CONF_CLIENT_ID: 1197489078, - } + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "cannot_connect"} async def test_duplicate_entry( @@ -154,8 +428,6 @@ async def test_duplicate_entry( mock_config_entry.add_to_hass(hass) - inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO) - await hass.async_block_till_done(wait_background_tasks=True) # Test we should not discover the already configured device @@ -169,30 +441,63 @@ async def test_duplicate_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000003"}, + user_input={ + CONF_ADDRESS: "00000000-0000-0000-0000-000000000003", + CONF_PIN: "1234", + }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_exception_connect( +async def test_exception_probe( hass: HomeAssistant, mock_automower_client: Mock, ) -> None: """Test we can select a device.""" - inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO) inject_bluetooth_service_info(hass, AUTOMOWER_UNNAMED_SERVICE_INFO) await hass.async_block_till_done(wait_background_tasks=True) mock_automower_client.probe_gatts.side_effect = BleakError result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0] - assert result["step_id"] == "confirm" + assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={}, + user_input={CONF_PIN: "1234"}, ) + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +async def test_exception_connect( + hass: HomeAssistant, + mock_automower_client: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can select a device.""" + + mock_config_entry.add_to_hass(hass) + + await hass.async_block_till_done(wait_background_tasks=True) + + mock_automower_client.connect.side_effect = BleakError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_ADDRESS: "00000000-0000-0000-0000-000000000001", + CONF_PIN: "1234", + }, + ) + + assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/husqvarna_automower_ble/test_init.py b/tests/components/husqvarna_automower_ble/test_init.py index 95a0a1f203710..341cc3c282fe8 100644 --- a/tests/components/husqvarna_automower_ble/test_init.py +++ b/tests/components/husqvarna_automower_ble/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components.husqvarna_automower_ble.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -39,7 +40,38 @@ async def test_setup( assert device_entry == snapshot -async def test_setup_retry_connect( +async def test_setup_missing_pin( + hass: HomeAssistant, + mock_automower_client: Mock, +) -> None: + """Test a setup that was created before PIN support.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + title="My home", + unique_id="397678e5-9995-4a39-9d9f-ae6ba310236c", + data={ + CONF_ADDRESS: "00000000-0000-0000-0000-000000000003", + CONF_CLIENT_ID: "1197489078", + }, + ) + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + hass.config_entries.async_update_entry( + mock_config_entry, + data={**mock_config_entry.data, CONF_PIN: 1234}, + ) + + assert len(hass.config_entries.flow.async_progress()) == 1 + await hass.async_block_till_done() + + +async def test_setup_failed_connect( hass: HomeAssistant, mock_automower_client: Mock, mock_config_entry: MockConfigEntry, @@ -68,3 +100,18 @@ async def test_setup_unknown_error( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_invalid_pin( + hass: HomeAssistant, + mock_automower_client: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unable to connect due to incorrect PIN.""" + mock_automower_client.connect.return_value = ResponseResult.INVALID_PIN + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR From 10bf1cb9990f54d8d4fa280e3110e85ac7710ddd Mon Sep 17 00:00:00 2001 From: wollew Date: Wed, 27 Aug 2025 10:15:52 +0200 Subject: [PATCH 238/247] Add DeviceInfo to Velux entities (#149575) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker --- .../components/velux/binary_sensor.py | 4 +- homeassistant/components/velux/cover.py | 8 ++-- homeassistant/components/velux/entity.py | 19 +++++++- homeassistant/components/velux/strings.json | 7 +++ tests/components/velux/test_binary_sensor.py | 48 ++++++++++++++++--- 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/velux/binary_sensor.py b/homeassistant/components/velux/binary_sensor.py index e08d4bcf545be..15d5d2c89add3 100644 --- a/homeassistant/components/velux/binary_sensor.py +++ b/homeassistant/components/velux/binary_sensor.py @@ -1,4 +1,4 @@ -"""Support for rain sensors build into some velux windows.""" +"""Support for rain sensors built into some Velux windows.""" from __future__ import annotations @@ -44,12 +44,12 @@ class VeluxRainSensor(VeluxEntity, BinarySensorEntity): _attr_should_poll = True # the rain sensor / opening limitations needs polling unlike the rest of the Velux devices _attr_entity_registry_enabled_default = False _attr_device_class = BinarySensorDeviceClass.MOISTURE + _attr_translation_key = "rain_sensor" def __init__(self, node: OpeningDevice, config_entry_id: str) -> None: """Initialize VeluxRainSensor.""" super().__init__(node, config_entry_id) self._attr_unique_id = f"{self._attr_unique_id}_rain_sensor" - self._attr_name = f"{node.name} Rain sensor" async def async_update(self) -> None: """Fetch the latest state from the device.""" diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index d6bf8905d91cb..32be29c3c9142 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -5,7 +5,7 @@ from typing import Any, cast from pyvlx import OpeningDevice, Position -from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter, Window +from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter from homeassistant.components.cover import ( ATTR_POSITION, @@ -44,9 +44,13 @@ class VeluxCover(VeluxEntity, CoverEntity): _is_blind = False node: OpeningDevice + # Do not name the "main" feature of the device (position control) + _attr_name = None + def __init__(self, node: OpeningDevice, config_entry_id: str) -> None: """Initialize VeluxCover.""" super().__init__(node, config_entry_id) + # Window is the default device class for covers self._attr_device_class = CoverDeviceClass.WINDOW if isinstance(node, Awning): self._attr_device_class = CoverDeviceClass.AWNING @@ -59,8 +63,6 @@ def __init__(self, node: OpeningDevice, config_entry_id: str) -> None: self._attr_device_class = CoverDeviceClass.GATE if isinstance(node, RollerShutter): self._attr_device_class = CoverDeviceClass.SHUTTER - if isinstance(node, Window): - self._attr_device_class = CoverDeviceClass.WINDOW @property def supported_features(self) -> CoverEntityFeature: diff --git a/homeassistant/components/velux/entity.py b/homeassistant/components/velux/entity.py index 1231a98e0a83c..fa06598f97958 100644 --- a/homeassistant/components/velux/entity.py +++ b/homeassistant/components/velux/entity.py @@ -3,13 +3,17 @@ from pyvlx import Node from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity +from .const import DOMAIN + class VeluxEntity(Entity): - """Abstraction for al Velux entities.""" + """Abstraction for all Velux entities.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, node: Node, config_entry_id: str) -> None: """Initialize the Velux device.""" @@ -19,7 +23,18 @@ def __init__(self, node: Node, config_entry_id: str) -> None: if node.serial_number else f"{config_entry_id}_{node.node_id}" ) - self._attr_name = node.name if node.name else f"#{node.node_id}" + self._attr_device_info = DeviceInfo( + identifiers={ + ( + DOMAIN, + node.serial_number + if node.serial_number + else f"{config_entry_id}_{node.node_id}", + ) + }, + name=node.name if node.name else f"#{node.node_id}", + serial_number=node.serial_number, + ) @callback def async_register_callbacks(self): diff --git a/homeassistant/components/velux/strings.json b/homeassistant/components/velux/strings.json index 0cf578732fbae..5123c59fe4330 100644 --- a/homeassistant/components/velux/strings.json +++ b/homeassistant/components/velux/strings.json @@ -27,5 +27,12 @@ "name": "Reboot gateway", "description": "Reboots the KLF200 Gateway." } + }, + "entity": { + "binary_sensor": { + "rain_sensor": { + "name": "Rain sensor" + } + } } } diff --git a/tests/components/velux/test_binary_sensor.py b/tests/components/velux/test_binary_sensor.py index 8eb065a5a46f8..dfe994b6fa2e3 100644 --- a/tests/components/velux/test_binary_sensor.py +++ b/tests/components/velux/test_binary_sensor.py @@ -8,6 +8,8 @@ from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry from tests.common import MockConfigEntry, async_fire_time_changed @@ -21,17 +23,15 @@ async def test_rain_sensor_state( freezer: FrozenDateTimeFactory, ) -> None: """Test the rain sensor.""" - mock_config_entry.add_to_hass(hass) - - test_entity_id = "binary_sensor.test_window_rain_sensor" - with ( - patch("homeassistant.components.velux.PLATFORMS", [Platform.BINARY_SENSOR]), - ): + mock_config_entry.add_to_hass(hass) + with patch("homeassistant.components.velux.PLATFORMS", [Platform.BINARY_SENSOR]): # setup config entry assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + test_entity_id = "binary_sensor.test_window_rain_sensor" + # simulate no rain detected freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -48,3 +48,39 @@ async def test_rain_sensor_state( state = hass.states.get(test_entity_id) assert state is not None assert state.state == STATE_ON + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.usefixtures("mock_module") +async def test_rain_sensor_device_association( + hass: HomeAssistant, + mock_window: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, +) -> None: + """Test the rain sensor is properly associated with its device.""" + + mock_config_entry.add_to_hass(hass) + with patch("homeassistant.components.velux.PLATFORMS", [Platform.BINARY_SENSOR]): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + test_entity_id = "binary_sensor.test_window_rain_sensor" + + # Verify entity exists + state = hass.states.get(test_entity_id) + assert state is not None + + # Get entity entry + entity_entry = entity_registry.async_get(test_entity_id) + assert entity_entry is not None + assert entity_entry.device_id is not None + + # Get device entry + device_entry = device_registry.async_get(entity_entry.device_id) + assert device_entry is not None + + # Verify device has correct identifiers + assert ("velux", mock_window.serial_number) in device_entry.identifiers + assert device_entry.name == mock_window.name From 8b10128c5099e4e19d74300b51e4b23d110f446a Mon Sep 17 00:00:00 2001 From: MosheL Date: Wed, 27 Aug 2025 11:16:29 +0300 Subject: [PATCH 239/247] Fix CCM15 temperature set always changes the ac_mode to cool (#134719) Co-authored-by: Franck Nijhof Co-authored-by: Joostlek Co-authored-by: Erik Montnemery --- homeassistant/components/ccm15/climate.py | 13 ++++--- homeassistant/components/ccm15/coordinator.py | 35 +++++++++++++------ homeassistant/components/ccm15/manifest.json | 2 +- requirements_all.txt | 6 ++-- requirements_test_all.txt | 6 ++-- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/ccm15/climate.py b/homeassistant/components/ccm15/climate.py index df321395b9e44..f4a68acc322aa 100644 --- a/homeassistant/components/ccm15/climate.py +++ b/homeassistant/components/ccm15/climate.py @@ -3,9 +3,10 @@ import logging from typing import Any -from ccm15 import CCM15DeviceState +from ccm15 import CCM15DeviceState, CCM15SlaveDevice from homeassistant.components.climate import ( + ATTR_HVAC_MODE, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -88,7 +89,7 @@ def __init__( ) @property - def data(self) -> CCM15DeviceState | None: + def data(self) -> CCM15SlaveDevice | None: """Return device data.""" return self.coordinator.get_ac_data(self._ac_index) @@ -144,15 +145,17 @@ def extra_state_attributes(self) -> dict[str, Any]: async def async_set_temperature(self, **kwargs: Any) -> None: """Set the target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None: - await self.coordinator.async_set_temperature(self._ac_index, temperature) + await self.coordinator.async_set_temperature( + self._ac_index, self.data, temperature, kwargs.get(ATTR_HVAC_MODE) + ) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the hvac mode.""" - await self.coordinator.async_set_hvac_mode(self._ac_index, hvac_mode) + await self.coordinator.async_set_hvac_mode(self._ac_index, self.data, hvac_mode) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set the fan mode.""" - await self.coordinator.async_set_fan_mode(self._ac_index, fan_mode) + await self.coordinator.async_set_fan_mode(self._ac_index, self.data, fan_mode) async def async_turn_off(self) -> None: """Turn off.""" diff --git a/homeassistant/components/ccm15/coordinator.py b/homeassistant/components/ccm15/coordinator.py index 03a59aa3f246c..ad3bbc41a06cb 100644 --- a/homeassistant/components/ccm15/coordinator.py +++ b/homeassistant/components/ccm15/coordinator.py @@ -55,9 +55,9 @@ async def _fetch_data(self) -> CCM15DeviceState: """Get the current status of all AC devices.""" return await self._ccm15.get_status_async() - async def async_set_state(self, ac_index: int, state: str, value: int) -> None: + async def async_set_state(self, ac_index: int, data) -> None: """Set new target states.""" - if await self._ccm15.async_set_state(ac_index, state, value): + if await self._ccm15.async_set_state(ac_index, data): await self.async_request_refresh() def get_ac_data(self, ac_index: int) -> CCM15SlaveDevice | None: @@ -67,17 +67,32 @@ def get_ac_data(self, ac_index: int) -> CCM15SlaveDevice | None: return None return self.data.devices[ac_index] - async def async_set_hvac_mode(self, ac_index, hvac_mode: HVACMode) -> None: - """Set the hvac mode.""" + async def async_set_hvac_mode( + self, ac_index: int, data: CCM15SlaveDevice, hvac_mode: HVACMode + ) -> None: + """Set the HVAC mode.""" _LOGGER.debug("Set Hvac[%s]='%s'", ac_index, str(hvac_mode)) - await self.async_set_state(ac_index, "mode", CONST_STATE_CMD_MAP[hvac_mode]) + data.ac_mode = CONST_STATE_CMD_MAP[hvac_mode] + await self.async_set_state(ac_index, data) - async def async_set_fan_mode(self, ac_index, fan_mode: str) -> None: + async def async_set_fan_mode( + self, ac_index: int, data: CCM15SlaveDevice, fan_mode: str + ) -> None: """Set the fan mode.""" _LOGGER.debug("Set Fan[%s]='%s'", ac_index, fan_mode) - await self.async_set_state(ac_index, "fan", CONST_FAN_CMD_MAP[fan_mode]) - - async def async_set_temperature(self, ac_index, temp) -> None: + data.fan_mode = CONST_FAN_CMD_MAP[fan_mode] + await self.async_set_state(ac_index, data) + + async def async_set_temperature( + self, + ac_index: int, + data: CCM15SlaveDevice, + temp: int, + hvac_mode: HVACMode | None, + ) -> None: """Set the target temperature mode.""" _LOGGER.debug("Set Temp[%s]='%s'", ac_index, temp) - await self.async_set_state(ac_index, "temp", temp) + data.temperature_setpoint = temp + if hvac_mode is not None: + data.ac_mode = CONST_STATE_CMD_MAP[hvac_mode] + await self.async_set_state(ac_index, data) diff --git a/homeassistant/components/ccm15/manifest.json b/homeassistant/components/ccm15/manifest.json index 2d985d6148aaa..23cd554796345 100644 --- a/homeassistant/components/ccm15/manifest.json +++ b/homeassistant/components/ccm15/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ccm15", "iot_class": "local_polling", - "requirements": ["py-ccm15==0.0.9"] + "requirements": ["py_ccm15==0.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8d732aac02947..4d702582df753 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1748,9 +1748,6 @@ py-aosmith==1.0.12 # homeassistant.components.canary py-canary==0.5.4 -# homeassistant.components.ccm15 -py-ccm15==0.0.9 - # homeassistant.components.cpuspeed py-cpuinfo==9.0.0 @@ -1823,6 +1820,9 @@ pyW215==0.8.0 # homeassistant.components.w800rf32 pyW800rf32==0.4 +# homeassistant.components.ccm15 +py_ccm15==0.1.2 + # homeassistant.components.ads pyads==3.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc00dc8fba41a..37a929bb8a06a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1474,9 +1474,6 @@ py-aosmith==1.0.12 # homeassistant.components.canary py-canary==0.5.4 -# homeassistant.components.ccm15 -py-ccm15==0.0.9 - # homeassistant.components.cpuspeed py-cpuinfo==9.0.0 @@ -1531,6 +1528,9 @@ pyTibber==0.31.6 # homeassistant.components.dlink pyW215==0.8.0 +# homeassistant.components.ccm15 +py_ccm15==0.1.2 + # homeassistant.components.hisense_aehw4a1 pyaehw4a1==0.3.9 From 2abb9148674a4d765eb34a164c3e5ec84c20ae4f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 Aug 2025 10:36:42 +0200 Subject: [PATCH 240/247] Update husqvarna_automower_ble bluetooth discovery checks (#151225) --- .../components/husqvarna_automower_ble/config_flow.py | 6 +----- tests/components/husqvarna_automower_ble/__init__.py | 3 --- .../husqvarna_automower_ble/test_config_flow.py | 8 +++++--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/husqvarna_automower_ble/config_flow.py b/homeassistant/components/husqvarna_automower_ble/config_flow.py index 15de6bde70825..c8f1cfaf63023 100644 --- a/homeassistant/components/husqvarna_automower_ble/config_flow.py +++ b/homeassistant/components/husqvarna_automower_ble/config_flow.py @@ -34,12 +34,8 @@ def _is_supported(discovery_info: BluetoothServiceInfo): service == "98bd0001-0b0e-421a-84e5-ddbf75dc6de4" for service in discovery_info.service_uuids ) - service_generic = any( - service == "00001800-0000-1000-8000-00805f9b34fb" - for service in discovery_info.service_uuids - ) - return manufacturer and service_husqvarna and service_generic + return manufacturer and service_husqvarna def _pin_valid(pin: str) -> bool: diff --git a/tests/components/husqvarna_automower_ble/__init__.py b/tests/components/husqvarna_automower_ble/__init__.py index 7ca5aea121d98..841b6f65516af 100644 --- a/tests/components/husqvarna_automower_ble/__init__.py +++ b/tests/components/husqvarna_automower_ble/__init__.py @@ -17,7 +17,6 @@ manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"}, service_uuids=[ "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", - "00001800-0000-1000-8000-00805f9b34fb", ], source="local", ) @@ -30,7 +29,6 @@ manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"}, service_uuids=[ "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", - "00001800-0000-1000-8000-00805f9b34fb", ], source="local", ) @@ -43,7 +41,6 @@ manufacturer_data={}, service_uuids=[ "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", - "00001800-0000-1000-8000-00805f9b34fb", ], source="local", ) diff --git a/tests/components/husqvarna_automower_ble/test_config_flow.py b/tests/components/husqvarna_automower_ble/test_config_flow.py index 41dfdffae7388..7b47063975e14 100644 --- a/tests/components/husqvarna_automower_ble/test_config_flow.py +++ b/tests/components/husqvarna_automower_ble/test_config_flow.py @@ -13,9 +13,9 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( + AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO, AUTOMOWER_SERVICE_INFO, AUTOMOWER_UNNAMED_SERVICE_INFO, - AUTOMOWER_UNSUPPORTED_GROUP_SERVICE_INFO, ) from tests.common import MockConfigEntry @@ -277,13 +277,15 @@ async def test_bluetooth_not_paired( async def test_bluetooth_invalid(hass: HomeAssistant) -> None: """Test bluetooth device discovery with invalid data.""" - inject_bluetooth_service_info(hass, AUTOMOWER_UNSUPPORTED_GROUP_SERVICE_INFO) + inject_bluetooth_service_info( + hass, AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO + ) await hass.async_block_till_done(wait_background_tasks=True) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, - data=AUTOMOWER_UNSUPPORTED_GROUP_SERVICE_INFO, + data=AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" From bfd4f8522554876f3a19fd9bbd2b8606356778a3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 Aug 2025 10:37:32 +0200 Subject: [PATCH 241/247] Fix husqvarna_automower_ble activity mapping (#151228) --- .../husqvarna_automower_ble/lawn_mower.py | 4 ++++ .../husqvarna_automower_ble/test_lawn_mower.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/husqvarna_automower_ble/lawn_mower.py b/homeassistant/components/husqvarna_automower_ble/lawn_mower.py index 78d39ddd96a62..ffe05bac8a888 100644 --- a/homeassistant/components/husqvarna_automower_ble/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower_ble/lawn_mower.py @@ -73,6 +73,10 @@ def _get_activity(self) -> LawnMowerActivity | None: if state in (MowerState.STOPPED, MowerState.OFF, MowerState.WAIT_FOR_SAFETYPIN): # This is actually stopped, but that isn't an option return LawnMowerActivity.ERROR + if state == MowerState.PENDING_START and activity == MowerActivity.NONE: + # This happens when the mower is safety stopped and we try to send a + # command to start it. + return LawnMowerActivity.ERROR if state in ( MowerState.RESTRICTED, MowerState.IN_OPERATION, diff --git a/tests/components/husqvarna_automower_ble/test_lawn_mower.py b/tests/components/husqvarna_automower_ble/test_lawn_mower.py index 2a127c785d972..25e02a43acc84 100644 --- a/tests/components/husqvarna_automower_ble/test_lawn_mower.py +++ b/tests/components/husqvarna_automower_ble/test_lawn_mower.py @@ -156,7 +156,7 @@ async def test_bleak_error_data_update( # Operational states are mapped according to the activity ( OPERATIONAL_STATES, - [MowerActivity.CHARGING, MowerActivity.NONE, MowerActivity.PARKED], + [MowerActivity.CHARGING, MowerActivity.PARKED], LawnMowerActivity.DOCKED, ), ( @@ -174,6 +174,17 @@ async def test_bleak_error_data_update( [MowerActivity.STOPPED_IN_GARDEN], LawnMowerActivity.ERROR, ), + # Special case for MowerActivity.NONE + ( + [MowerState.IN_OPERATION, MowerState.RESTRICTED], + [MowerActivity.NONE], + LawnMowerActivity.DOCKED, + ), + ( + [MowerState.PENDING_START], + [MowerActivity.NONE], + LawnMowerActivity.ERROR, + ), ], ) async def test_mower_activity_mapping( From 85f3f180abfdd6a8e01a235cd7b61e62ef0123a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 Aug 2025 10:43:41 +0200 Subject: [PATCH 242/247] Fix stale comment in device registry (#151227) --- homeassistant/helpers/device_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index e25ca11e08330..5e5f50c96fc5a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -985,7 +985,7 @@ def async_get_or_create( via_device_id=via_device_id, ) - # This is safe because _async_update_device will always return a device + # This is safe because async_update_device will always return a device # in this use case. assert device return device From 3a48c9569c654e413597079c3eead792f707c06e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 Aug 2025 10:43:59 +0200 Subject: [PATCH 243/247] Fix stale comment in entity registry (#151226) --- homeassistant/helpers/entity_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 9b619385d8ccc..571f914e9d307 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1187,7 +1187,7 @@ def async_device_modified( return # Ignore device disabled by config entry, this is handled by - # async_config_entry_disabled + # async_config_entry_disabled_by_changed if device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY: return From fc1c0d22b9e890b2ea5e4bd36f6b2aafc3562db4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 27 Aug 2025 10:45:00 +0200 Subject: [PATCH 244/247] Add online status to Tuya debug log (#151222) --- homeassistant/components/tuya/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 6ed8f0253ab47..fc408531a386f 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -154,8 +154,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool device_registry = dr.async_get(hass) for device in manager.device_map.values(): LOGGER.debug( - "Register device %s: %s (function: %s, status range: %s)", + "Register device %s (online: %s): %s (function: %s, status range: %s)", device.id, + device.online, device.status, device.function, device.status_range, @@ -231,9 +232,10 @@ def update_device( ) -> None: """Update device status.""" LOGGER.debug( - "Received update for device %s: %s (updated properties: %s)", + "Received update for device %s (online: %s): %s (updated properties: %s)", device.id, - self.manager.device_map[device.id].status, + device.online, + device.status, updated_status_properties, ) dispatcher_send( @@ -248,8 +250,9 @@ def add_device(self, device: CustomerDevice) -> None: self.hass.add_job(self.async_remove_device, device.id) LOGGER.debug( - "Add device %s: %s (function: %s, status range: %s)", + "Add device %s (online: %s): %s (function: %s, status range: %s)", device.id, + device.online, device.status, device.function, device.status_range, From 43a1a679f9784170c4174052d8e463cff48a5818 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 27 Aug 2025 04:49:38 -0400 Subject: [PATCH 245/247] Add object_id to modern template syntax (#150489) Co-authored-by: Martin Hjelmare --- homeassistant/components/template/const.py | 1 + tests/components/template/test_template_entity.py | 10 ++++++++++ tests/components/template/test_trigger_entity.py | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 5ff2c0137ac41..23b3608d5e066 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -26,6 +26,7 @@ vol.Optional(CONF_NAME): cv.template, vol.Optional(CONF_PICTURE): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_OBJECT_ID): cv.string, } ) diff --git a/tests/components/template/test_template_entity.py b/tests/components/template/test_template_entity.py index 7fe3870ae1e43..f9dd18a486686 100644 --- a/tests/components/template/test_template_entity.py +++ b/tests/components/template/test_template_entity.py @@ -18,3 +18,13 @@ async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None: entity.add_template_attribute("_hello", tpl_with_hass) assert len(entity._template_attrs.get(tpl_with_hass, [])) == 1 + + +async def test_object_id(hass: HomeAssistant) -> None: + """Test template entity creates suggested entity_id from the object_id.""" + + class TemplateTest(template_entity.TemplateEntity): + _entity_id_format = "test.{}" + + entity = TemplateTest(hass, {"object_id": "test"}, "a") + assert entity.entity_id == "test.test" diff --git a/tests/components/template/test_trigger_entity.py b/tests/components/template/test_trigger_entity.py index 65db69fa2b912..000206c0788c3 100644 --- a/tests/components/template/test_trigger_entity.py +++ b/tests/components/template/test_trigger_entity.py @@ -17,6 +17,7 @@ class TestEntity(trigger_entity.TriggerEntity): """Test entity class.""" __test__ = False + _entity_id_format = "test.{}" extra_template_keys = (CONF_STATE,) @property @@ -134,3 +135,10 @@ async def test_script_variables_from_coordinator(hass: HomeAssistant) -> None: coordinator._execute_update({"value": STATE_ON}) assert entity._render_script_variables() == {"value": STATE_ON} + + +async def test_object_id(hass: HomeAssistant) -> None: + """Test template entity creates suggested entity_id from the object_id.""" + coordinator = TriggerUpdateCoordinator(hass, {}) + entity = TestEntity(hass, coordinator, {"object_id": "test"}) + assert entity.entity_id == "test.test" From e894a03c432096d519ca73c9a1a4e36489a0cb08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Wed, 27 Aug 2025 10:57:25 +0200 Subject: [PATCH 246/247] Person: Use the home zone lat/lon coordinates when detected home by a stationary tracker (#134075) Co-authored-by: Erik Montnemery --- homeassistant/components/person/__init__.py | 25 ++++-- homeassistant/components/person/manifest.json | 2 +- tests/components/person/test_init.py | 82 +++++++++++++------ 3 files changed, 77 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 0dd8646b17ed0..46e9a121649c3 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -15,6 +15,7 @@ DOMAIN as DEVICE_TRACKER_DOMAIN, SourceType, ) +from homeassistant.components.zone import ENTITY_ID_HOME from homeassistant.const import ( ATTR_EDITABLE, ATTR_GPS_ACCURACY, @@ -464,7 +465,7 @@ async def async_added_to_hass(self) -> None: """Register device trackers.""" await super().async_added_to_hass() if state := await self.async_get_last_state(): - self._parse_source_state(state) + self._parse_source_state(state, state) if self.hass.is_running: # Update person now if hass is already running. @@ -514,7 +515,7 @@ def _async_handle_tracker_update(self, event: Event[EventStateChangedData]) -> N @callback def _update_state(self) -> None: """Update the state.""" - latest_non_gps_home = latest_not_home = latest_gps = latest = None + latest_non_gps_home = latest_not_home = latest_gps = latest = coordinates = None for entity_id in self._config[CONF_DEVICE_TRACKERS]: state = self.hass.states.get(entity_id) @@ -530,13 +531,23 @@ def _update_state(self) -> None: if latest_non_gps_home: latest = latest_non_gps_home + if ( + latest_non_gps_home.attributes.get(ATTR_LATITUDE) is None + and latest_non_gps_home.attributes.get(ATTR_LONGITUDE) is None + and (home_zone := self.hass.states.get(ENTITY_ID_HOME)) + ): + coordinates = home_zone + else: + coordinates = latest_non_gps_home elif latest_gps: latest = latest_gps + coordinates = latest_gps else: latest = latest_not_home + coordinates = latest_not_home - if latest: - self._parse_source_state(latest) + if latest and coordinates: + self._parse_source_state(latest, coordinates) else: self._attr_state = None self._source = None @@ -548,15 +559,15 @@ def _update_state(self) -> None: self.async_write_ha_state() @callback - def _parse_source_state(self, state: State) -> None: + def _parse_source_state(self, state: State, coordinates: State) -> None: """Parse source state and set person attributes. This is a device tracker state or the restored person state. """ self._attr_state = state.state self._source = state.entity_id - self._latitude = state.attributes.get(ATTR_LATITUDE) - self._longitude = state.attributes.get(ATTR_LONGITUDE) + self._latitude = coordinates.attributes.get(ATTR_LATITUDE) + self._longitude = coordinates.attributes.get(ATTR_LONGITUDE) self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY) @callback diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index 0c1792e927739..46ccf85db4a69 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -2,7 +2,7 @@ "domain": "person", "name": "Person", "codeowners": [], - "dependencies": ["image_upload", "http"], + "dependencies": ["image_upload", "http", "zone"], "documentation": "https://www.home-assistant.io/integrations/person", "integration_type": "system", "iot_class": "calculated", diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index c001da86adb95..81b38f59a3d62 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -14,7 +14,9 @@ DOMAIN, ) from homeassistant.const import ( + ATTR_EDITABLE, ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, ATTR_GPS_ACCURACY, ATTR_ID, ATTR_LATITUDE, @@ -112,14 +114,19 @@ async def test_setup_tracker(hass: HomeAssistant, hass_admin_user: MockUser) -> } assert await async_setup_component(hass, DOMAIN, config) + expected_attributes = { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_USER_ID: user_id, + } + state = hass.states.get("person.tracked_person") assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_SOURCE) is None - assert state.attributes.get(ATTR_USER_ID) == user_id + assert state.attributes == expected_attributes + # Test home without coordinates hass.states.async_set(DEVICE_TRACKER, "home") await hass.async_block_till_done() @@ -131,13 +138,41 @@ async def test_setup_tracker(hass: HomeAssistant, hass_admin_user: MockUser) -> state = hass.states.get("person.tracked_person") assert state.state == "home" - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER - assert state.attributes.get(ATTR_USER_ID) == user_id - assert state.attributes.get(ATTR_DEVICE_TRACKERS) == [DEVICE_TRACKER] + assert state.attributes == expected_attributes | { + ATTR_LATITUDE: 32.87336, + ATTR_LONGITUDE: -117.22743, + ATTR_SOURCE: DEVICE_TRACKER, + } + # Test home with coordinates + hass.states.async_set( + DEVICE_TRACKER, + "home", + {ATTR_LATITUDE: 10.123456, ATTR_LONGITUDE: 11.123456, ATTR_GPS_ACCURACY: 10}, + ) + await hass.async_block_till_done() + + state = hass.states.get("person.tracked_person") + assert state.state == "home" + assert state.attributes == expected_attributes | { + ATTR_GPS_ACCURACY: 10, + ATTR_LATITUDE: 10.123456, + ATTR_LONGITUDE: 11.123456, + ATTR_SOURCE: DEVICE_TRACKER, + } + + # Test not_home without coordinates + hass.states.async_set( + DEVICE_TRACKER, + "not_home", + ) + await hass.async_block_till_done() + + state = hass.states.get("person.tracked_person") + assert state.state == "not_home" + assert state.attributes == expected_attributes | {ATTR_SOURCE: DEVICE_TRACKER} + + # Test not_home with coordinates hass.states.async_set( DEVICE_TRACKER, "not_home", @@ -147,13 +182,12 @@ async def test_setup_tracker(hass: HomeAssistant, hass_admin_user: MockUser) -> state = hass.states.get("person.tracked_person") assert state.state == "not_home" - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) == 10.123456 - assert state.attributes.get(ATTR_LONGITUDE) == 11.123456 - assert state.attributes.get(ATTR_GPS_ACCURACY) == 10 - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER - assert state.attributes.get(ATTR_USER_ID) == user_id - assert state.attributes.get(ATTR_DEVICE_TRACKERS) == [DEVICE_TRACKER] + assert state.attributes == expected_attributes | { + ATTR_GPS_ACCURACY: 10, + ATTR_LATITUDE: 10.123456, + ATTR_LONGITUDE: 11.123456, + ATTR_SOURCE: DEVICE_TRACKER, + } async def test_setup_two_trackers( @@ -188,8 +222,8 @@ async def test_setup_two_trackers( state = hass.states.get("person.tracked_person") assert state.state == "home" assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_LATITUDE) == 32.87336 + assert state.attributes.get(ATTR_LONGITUDE) == -117.22743 assert state.attributes.get(ATTR_GPS_ACCURACY) is None assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER assert state.attributes.get(ATTR_USER_ID) == user_id @@ -453,8 +487,8 @@ async def test_load_person_storage( state = hass.states.get("person.tracked_person") assert state.state == "home" assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_LATITUDE) == 32.87336 + assert state.attributes.get(ATTR_LONGITUDE) == -117.22743 assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id @@ -817,7 +851,7 @@ async def test_reload(hass: HomeAssistant, hass_admin_user: MockUser) -> None: }, ) - assert len(hass.states.async_entity_ids()) == 2 + assert len(hass.states.async_entity_ids()) == 3 # Person1, Person2, zone.home state_1 = hass.states.get("person.person_1") state_2 = hass.states.get("person.person_2") @@ -847,7 +881,7 @@ async def test_reload(hass: HomeAssistant, hass_admin_user: MockUser) -> None: ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids()) == 2 + assert len(hass.states.async_entity_ids()) == 3 # Person1, Person2, zone.home state_1 = hass.states.get("person.person_1") state_2 = hass.states.get("person.person_2") From 0d29b2d5a7b79649480ea305eca8367d0c767579 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 27 Aug 2025 11:00:31 +0200 Subject: [PATCH 247/247] Add MQTT alarm control panel subentry support (#150395) Co-authored-by: Norbert Rittel --- .../components/mqtt/alarm_control_panel.py | 90 ++++----- homeassistant/components/mqtt/config_flow.py | 188 +++++++++++++++++- homeassistant/components/mqtt/const.py | 34 +++- homeassistant/components/mqtt/strings.json | 53 ++++- tests/components/mqtt/common.py | 84 ++++++++ tests/components/mqtt/test_config_flow.py | 185 ++++++++++++++++- 6 files changed, 578 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 64b1a6b05fa29..72b92cdcb9d61 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -7,10 +7,7 @@ import voluptuous as vol from homeassistant.components import alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import ( - AlarmControlPanelEntityFeature, - AlarmControlPanelState, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelState from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CODE, CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant, callback @@ -21,12 +18,33 @@ from . import subscription from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import ( + ALARM_CONTROL_PANEL_SUPPORTED_FEATURES, + CONF_CODE_ARM_REQUIRED, + CONF_CODE_DISARM_REQUIRED, + CONF_CODE_TRIGGER_REQUIRED, CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_ARM_AWAY, + CONF_PAYLOAD_ARM_CUSTOM_BYPASS, + CONF_PAYLOAD_ARM_HOME, + CONF_PAYLOAD_ARM_NIGHT, + CONF_PAYLOAD_ARM_VACATION, + CONF_PAYLOAD_DISARM, + CONF_PAYLOAD_TRIGGER, CONF_RETAIN, CONF_STATE_TOPIC, CONF_SUPPORTED_FEATURES, + DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE, + DEFAULT_PAYLOAD_ARM_AWAY, + DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS, + DEFAULT_PAYLOAD_ARM_HOME, + DEFAULT_PAYLOAD_ARM_NIGHT, + DEFAULT_PAYLOAD_ARM_VACATION, + DEFAULT_PAYLOAD_DISARM, + DEFAULT_PAYLOAD_TRIGGER, PAYLOAD_NONE, + REMOTE_CODE, + REMOTE_CODE_TEXT, ) from .entity import MqttEntity, async_setup_entity_entry_helper from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage @@ -37,26 +55,6 @@ PARALLEL_UPDATES = 0 -_SUPPORTED_FEATURES = { - "arm_home": AlarmControlPanelEntityFeature.ARM_HOME, - "arm_away": AlarmControlPanelEntityFeature.ARM_AWAY, - "arm_night": AlarmControlPanelEntityFeature.ARM_NIGHT, - "arm_vacation": AlarmControlPanelEntityFeature.ARM_VACATION, - "arm_custom_bypass": AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, - "trigger": AlarmControlPanelEntityFeature.TRIGGER, -} - -CONF_CODE_ARM_REQUIRED = "code_arm_required" -CONF_CODE_DISARM_REQUIRED = "code_disarm_required" -CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required" -CONF_PAYLOAD_DISARM = "payload_disarm" -CONF_PAYLOAD_ARM_HOME = "payload_arm_home" -CONF_PAYLOAD_ARM_AWAY = "payload_arm_away" -CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night" -CONF_PAYLOAD_ARM_VACATION = "payload_arm_vacation" -CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass" -CONF_PAYLOAD_TRIGGER = "payload_trigger" - MQTT_ALARM_ATTRIBUTES_BLOCKED = frozenset( { alarm.ATTR_CHANGED_BY, @@ -65,44 +63,40 @@ } ) -DEFAULT_COMMAND_TEMPLATE = "{{action}}" -DEFAULT_ARM_NIGHT = "ARM_NIGHT" -DEFAULT_ARM_VACATION = "ARM_VACATION" -DEFAULT_ARM_AWAY = "ARM_AWAY" -DEFAULT_ARM_HOME = "ARM_HOME" -DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS" -DEFAULT_DISARM = "DISARM" -DEFAULT_TRIGGER = "TRIGGER" DEFAULT_NAME = "MQTT Alarm" -REMOTE_CODE = "REMOTE_CODE" -REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" - PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_SUPPORTED_FEATURES, default=list(_SUPPORTED_FEATURES)): [ - vol.In(_SUPPORTED_FEATURES) - ], + vol.Optional( + CONF_SUPPORTED_FEATURES, + default=list(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES), + ): [vol.In(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES)], vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_TRIGGER_REQUIRED, default=True): cv.boolean, vol.Optional( - CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE + CONF_COMMAND_TEMPLATE, default=DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE ): cv.template, vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_NAME): vol.Any(cv.string, None), - vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string, vol.Optional( - CONF_PAYLOAD_ARM_VACATION, default=DEFAULT_ARM_VACATION + CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_PAYLOAD_ARM_AWAY + ): cv.string, + vol.Optional( + CONF_PAYLOAD_ARM_HOME, default=DEFAULT_PAYLOAD_ARM_HOME + ): cv.string, + vol.Optional( + CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_PAYLOAD_ARM_NIGHT + ): cv.string, + vol.Optional( + CONF_PAYLOAD_ARM_VACATION, default=DEFAULT_PAYLOAD_ARM_VACATION ): cv.string, vol.Optional( - CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS + CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS ): cv.string, - vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, - vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_TRIGGER): cv.string, + vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_PAYLOAD_DISARM): cv.string, + vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_PAYLOAD_TRIGGER): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -152,7 +146,9 @@ def _setup_from_config(self, config: ConfigType) -> None: ).async_render for feature in self._config[CONF_SUPPORTED_FEATURES]: - self._attr_supported_features |= _SUPPORTED_FEATURES[feature] + self._attr_supported_features |= ALARM_CONTROL_PANEL_SUPPORTED_FEATURES[ + feature + ] if (code := self._config.get(CONF_CODE)) is None: self._attr_code_format = None diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8a4c2e95384c..b85b01f92c384 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -71,6 +71,7 @@ ATTR_SW_VERSION, CONF_BRIGHTNESS, CONF_CLIENT_ID, + CONF_CODE, CONF_DEVICE, CONF_DEVICE_CLASS, CONF_DISCOVERY, @@ -129,6 +130,7 @@ from .addon import get_addon_manager from .client import MqttClientSetup from .const import ( + ALARM_CONTROL_PANEL_SUPPORTED_FEATURES, ATTR_PAYLOAD, ATTR_QOS, ATTR_RETAIN, @@ -149,7 +151,10 @@ CONF_CERTIFICATE, CONF_CLIENT_CERT, CONF_CLIENT_KEY, + CONF_CODE_ARM_REQUIRED, + CONF_CODE_DISARM_REQUIRED, CONF_CODE_FORMAT, + CONF_CODE_TRIGGER_REQUIRED, CONF_COLOR_MODE_STATE_TOPIC, CONF_COLOR_MODE_VALUE_TEMPLATE, CONF_COLOR_TEMP_COMMAND_TEMPLATE, @@ -216,6 +221,11 @@ CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, CONF_OSCILLATION_VALUE_TEMPLATE, + CONF_PAYLOAD_ARM_AWAY, + CONF_PAYLOAD_ARM_CUSTOM_BYPASS, + CONF_PAYLOAD_ARM_HOME, + CONF_PAYLOAD_ARM_NIGHT, + CONF_PAYLOAD_ARM_VACATION, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_CLOSE, CONF_PAYLOAD_LOCK, @@ -229,6 +239,7 @@ CONF_PAYLOAD_RESET_PRESET_MODE, CONF_PAYLOAD_STOP, CONF_PAYLOAD_STOP_TILT, + CONF_PAYLOAD_TRIGGER, CONF_PAYLOAD_UNLOCK, CONF_PERCENTAGE_COMMAND_TEMPLATE, CONF_PERCENTAGE_COMMAND_TOPIC, @@ -280,6 +291,7 @@ CONF_STATE_VALUE_TEMPLATE, CONF_SUGGESTED_DISPLAY_PRECISION, CONF_SUPPORTED_COLOR_MODES, + CONF_SUPPORTED_FEATURES, CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE, CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC, CONF_SWING_HORIZONTAL_MODE_LIST, @@ -329,12 +341,18 @@ CONF_XY_VALUE_TEMPLATE, CONFIG_ENTRY_MINOR_VERSION, CONFIG_ENTRY_VERSION, + DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE, DEFAULT_BIRTH, DEFAULT_CLIMATE_INITIAL_TEMPERATURE, DEFAULT_DISCOVERY, DEFAULT_ENCODING, DEFAULT_KEEPALIVE, DEFAULT_ON_COMMAND_TYPE, + DEFAULT_PAYLOAD_ARM_AWAY, + DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS, + DEFAULT_PAYLOAD_ARM_HOME, + DEFAULT_PAYLOAD_ARM_NIGHT, + DEFAULT_PAYLOAD_ARM_VACATION, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_CLOSE, DEFAULT_PAYLOAD_LOCK, @@ -347,6 +365,7 @@ DEFAULT_PAYLOAD_PRESS, DEFAULT_PAYLOAD_RESET, DEFAULT_PAYLOAD_STOP, + DEFAULT_PAYLOAD_TRIGGER, DEFAULT_PAYLOAD_UNLOCK, DEFAULT_PORT, DEFAULT_POSITION_CLOSED, @@ -370,6 +389,8 @@ DEFAULT_WILL, DEFAULT_WS_PATH, DOMAIN, + REMOTE_CODE, + REMOTE_CODE_TEXT, SUPPORTED_PROTOCOLS, TRANSPORT_TCP, TRANSPORT_WEBSOCKETS, @@ -468,6 +489,7 @@ # Subentry selectors SUBENTRY_PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, @@ -573,6 +595,21 @@ NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0) ) +# Alarm control panel selectors +ALARM_CONTROL_PANEL_FEATURES_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=list(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES), + multiple=True, + translation_key="alarm_control_panel_features", + ) +) +ALARM_CONTROL_PANEL_CODE_MODE = SelectSelector( + SelectSelectorConfig( + options=["local_code", "remote_code", "remote_code_text"], + translation_key="alarm_control_panel_code_mode", + ) +) + # Climate specific selectors CLIMATE_MODE_SELECTOR = SelectSelector( SelectSelectorConfig( @@ -729,6 +766,25 @@ def configured_target_temperature_feature(config: dict[str, Any]) -> str: vol.Coerce(int), ) +_CODE_VALIDATION_MODE = { + "remote_code": REMOTE_CODE, + "remote_code_text": REMOTE_CODE_TEXT, +} + + +@callback +def default_alarm_control_panel_code(config: dict[str, Any]) -> str: + """Return alarm control panel code based on the stored code and code mode.""" + code: str + if config["alarm_control_panel_code_mode"] in _CODE_VALIDATION_MODE: + # Return magic value for remote code validation + return _CODE_VALIDATION_MODE[config["alarm_control_panel_code_mode"]] + if (code := config.get(CONF_CODE, "")) in _CODE_VALIDATION_MODE.values(): + # Remove magic value for remote code validation + return "" + + return code + @callback def temperature_default_from_celsius_to_system_default( @@ -925,6 +981,7 @@ class PlatformField: vol.UNDEFINED ) is_schema_default: bool = False + include_in_config: bool = False exclude_from_reconfig: bool = False exclude_from_config: bool = False conditions: tuple[dict[str, Any], ...] | None = None @@ -995,6 +1052,23 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: } PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { + Platform.ALARM_CONTROL_PANEL.value: { + CONF_SUPPORTED_FEATURES: PlatformField( + selector=ALARM_CONTROL_PANEL_FEATURES_SELECTOR, + required=True, + default=lambda config: config.get( + CONF_SUPPORTED_FEATURES, list(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES) + ), + ), + "alarm_control_panel_code_mode": PlatformField( + selector=ALARM_CONTROL_PANEL_CODE_MODE, + required=True, + exclude_from_config=True, + default=lambda config: config[CONF_CODE].lower() + if config.get(CONF_CODE) in (REMOTE_CODE, REMOTE_CODE_TEXT) + else "local_code", + ), + }, Platform.BINARY_SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR, @@ -1168,6 +1242,92 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: Platform.LOCK.value: {}, } PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { + Platform.ALARM_CONTROL_PANEL: { + CONF_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + default=DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_CODE: PlatformField( + selector=PASSWORD_SELECTOR, + required=True, + include_in_config=True, + default=default_alarm_control_panel_code, + conditions=({"alarm_control_panel_code_mode": "local_code"},), + ), + CONF_CODE_ARM_REQUIRED: PlatformField( + selector=BOOLEAN_SELECTOR, + required=True, + default=True, + ), + CONF_CODE_DISARM_REQUIRED: PlatformField( + selector=BOOLEAN_SELECTOR, + required=True, + default=True, + ), + CONF_CODE_TRIGGER_REQUIRED: PlatformField( + selector=BOOLEAN_SELECTOR, + required=True, + default=True, + ), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + CONF_PAYLOAD_ARM_HOME: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_HOME, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_ARM_AWAY: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_AWAY, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_ARM_NIGHT: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_NIGHT, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_ARM_VACATION: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_VACATION, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_ARM_CUSTOM_BYPASS: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_TRIGGER: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_TRIGGER, + section="alarm_control_panel_payload_settings", + ), + }, Platform.BINARY_SENSOR.value: { CONF_STATE_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -2774,6 +2934,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: str, Callable[[dict[str, Any]], dict[str, str]] | None, ] = { + Platform.ALARM_CONTROL_PANEL: None, Platform.BINARY_SENSOR.value: None, Platform.BUTTON.value: None, Platform.CLIMATE.value: validate_climate_platform_config, @@ -2969,13 +3130,24 @@ def get_default(field_details: PlatformField) -> Any: data_schema: dict[Any, Any] = {} all_data_element_options: set[Any] = set() no_reconfig_options: set[Any] = set() + + defaults: dict[str, Any] = {} + for field_name, field_details in data_schema_fields.items(): + default = defaults[field_name] = get_default(field_details) + if not field_details.include_in_config or component_data is None: + continue + component_data[field_name] = default + for schema_section in sections: + # Always calculate the default values + # Getting the default value may update the subentry data, + # even when and option is filtered out data_schema_element = { - vol.Required(field_name, default=get_default(field_details)) + vol.Required(field_name, default=defaults[field_name]) if field_details.required else vol.Optional( field_name, - default=get_default(field_details) + default=defaults[field_name] if field_details.default is not None else vol.UNDEFINED, ): field_details.selector(component_data_with_user_input or {}) @@ -3024,12 +3196,16 @@ def get_default(field_details: PlatformField) -> Any: ) # Reset all fields from the component_data not in the schema + # except for options that should stay included if component_data: filtered_fields = ( set(data_schema_fields) - all_data_element_options - no_reconfig_options ) for field in filtered_fields: - if field in component_data: + if ( + field in component_data + and not data_schema_fields[field].include_in_config + ): del component_data[field] return vol.Schema(data_schema) @@ -3591,6 +3767,7 @@ def update_component_fields( for field, platform_field in data_schema_fields.items() if field in (set(component_data) - set(config)) and not platform_field.exclude_from_reconfig + and not platform_field.include_in_config ): component_data.pop(field) component_data.update(merged_user_input) @@ -3906,7 +4083,10 @@ def _async_update_component_data_defaults(self) -> None: ) component_data.update(subentry_default_data) for key, platform_field in platform_fields.items(): - if not platform_field.exclude_from_config: + if ( + not platform_field.exclude_from_config + or platform_field.include_in_config + ): continue if key in component_data: component_data.pop(key) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 2128b55c4b0a7..d1feb25b28139 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -4,6 +4,7 @@ import jinja2 +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.const import CONF_DISCOVERY, CONF_PAYLOAD, Platform from homeassistant.exceptions import TemplateError @@ -31,7 +32,10 @@ CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" +CONF_CODE_ARM_REQUIRED = "code_arm_required" +CONF_CODE_DISARM_REQUIRED = "code_disarm_required" CONF_CODE_FORMAT = "code_format" +CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required" CONF_COMMAND_TEMPLATE = "command_template" CONF_COMMAND_TOPIC = "command_topic" CONF_DISCOVERY_PREFIX = "discovery_prefix" @@ -127,7 +131,13 @@ CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template" CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" +CONF_PAYLOAD_ARM_AWAY = "payload_arm_away" +CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass" +CONF_PAYLOAD_ARM_HOME = "payload_arm_home" +CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night" +CONF_PAYLOAD_ARM_VACATION = "payload_arm_vacation" CONF_PAYLOAD_CLOSE = "payload_close" +CONF_PAYLOAD_DISARM = "payload_disarm" CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_OPEN = "payload_open" CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" @@ -137,6 +147,7 @@ CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" CONF_PAYLOAD_STOP = "payload_stop" CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt" +CONF_PAYLOAD_TRIGGER = "payload_trigger" CONF_PAYLOAD_UNLOCK = "payload_unlock" CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -247,6 +258,7 @@ CONF_OBJECT_ID = "object_id" CONF_SUPPORT_URL = "support_url" +DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE = "{{action}}" DEFAULT_BRIGHTNESS = False DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_CLIMATE_INITIAL_TEMPERATURE = 21.0 @@ -260,8 +272,15 @@ DEFAULT_OPTIMISTIC = False DEFAULT_ON_COMMAND_TYPE = "last" DEFAULT_QOS = 0 + +DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY" +DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS" +DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME" +DEFAULT_PAYLOAD_ARM_NIGHT = "ARM_NIGHT" +DEFAULT_PAYLOAD_ARM_VACATION = "ARM_VACATION" DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_CLOSE = "CLOSE" +DEFAULT_PAYLOAD_DISARM = "DISARM" DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_PAYLOAD_OFF = "OFF" @@ -270,10 +289,10 @@ DEFAULT_PAYLOAD_OSCILLATE_OFF = "oscillate_off" DEFAULT_PAYLOAD_OSCILLATE_ON = "oscillate_on" DEFAULT_PAYLOAD_PRESS = "PRESS" -DEFAULT_PAYLOAD_STOP = "STOP" DEFAULT_PAYLOAD_RESET = "None" +DEFAULT_PAYLOAD_STOP = "STOP" +DEFAULT_PAYLOAD_TRIGGER = "TRIGGER" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" - DEFAULT_PORT = 1883 DEFAULT_RETAIN = False DEFAULT_TILT_CLOSED_POSITION = 0 @@ -303,6 +322,17 @@ VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] +ALARM_CONTROL_PANEL_SUPPORTED_FEATURES = { + "arm_home": AlarmControlPanelEntityFeature.ARM_HOME, + "arm_away": AlarmControlPanelEntityFeature.ARM_AWAY, + "arm_night": AlarmControlPanelEntityFeature.ARM_NIGHT, + "arm_vacation": AlarmControlPanelEntityFeature.ARM_VACATION, + "arm_custom_bypass": AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + "trigger": AlarmControlPanelEntityFeature.TRIGGER, +} +REMOTE_CODE = "REMOTE_CODE" +REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" + PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" PROTOCOL_5 = "5" diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 3844cf8d669f3..fa615ed1f9194 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -243,6 +243,7 @@ "title": "Configure MQTT device \"{mqtt_device}\"", "description": "Please configure specific details for {platform} entity \"{entity}\":", "data": { + "alarm_control_panel_code_mode": "Alarm code validation mode", "climate_feature_action": "Current action support", "climate_feature_current_humidity": "Current humidity support", "climate_feature_current_temperature": "Current temperature support", @@ -263,10 +264,12 @@ "schema": "Schema", "state_class": "State class", "suggested_display_precision": "Suggested display precision", + "supported_features": "Supported features", "temperature_unit": "Temperature unit", "unit_of_measurement": "Unit of measurement" }, "data_description": { + "alarm_control_panel_code_mode": "Configures how the alarm control panel validates the code. A local code is configured with the entity and is validated by Home Assistant. A remote code is sent to the device and validated remotely. [Learn more.]({url}#code)", "climate_feature_action": "The climate supports reporting the current action.", "climate_feature_current_humidity": "The climate supports reporting the current humidity.", "climate_feature_current_temperature": "The climate supports reporting the current temperature.", @@ -287,6 +290,7 @@ "schema": "The schema to use. [Learn more.]({url}#comparison-of-light-mqtt-schemas)", "state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)", "suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)", + "supported_features": "The features that the entity supports.", "temperature_unit": "This determines the native unit of measurement the MQTT climate device works with.", "unit_of_measurement": "Defines the unit of measurement of the sensor, if any." }, @@ -308,7 +312,11 @@ "data": { "blue_template": "Blue template", "brightness_template": "Brightness template", + "code": "Alarm code", "code_format": "Code format", + "code_arm_required": "Code arm required", + "code_disarm_required": "Code disarm required", + "code_trigger_required": "Code trigger required", "command_template": "Command template", "command_topic": "Command topic", "command_off_template": "Command \"off\" template", @@ -341,10 +349,14 @@ "data_description": { "blue_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract blue color from the state payload value. Expected result of the template is an integer from 0-255 range.", "brightness_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract brightness from the state payload value. Expected result of the template is an integer from 0-255 range.", + "code": "Specifies a code to enable or disable the alarm in the frontend. Note that this blocks sending MQTT message commands to the remote device if the code validation fails. [Learn more.]({url}#code)", "code_format": "A regular expression to validate a supplied code when it is set during the action to open, lock or unlock the MQTT lock. [Learn more.]({url}#code_format)", + "code_arm_required": "If set, the code is required to arm the alarm. If not set, the code is not validated.", + "code_disarm_required": "If set, the code is required to disarm the alarm. If not set, the code is not validated.", + "code_trigger_required": "If set, the code is required to manually trigger the alarm. If not set, the code is not validated.", "command_off_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"off\" state changes. Available variables are: `state` and `transition`.", "command_on_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"on\" state changes. Available variables: `state`, `brightness`, `color_temp`, `red`, `green`, `blue`, `hue`, `sat`, `flash`, `transition` and `effect`. Values `red`, `green`, `blue` and `brightness` are provided as integers from range 0-255. Value of `hue` is provided as float from range 0-360. Value of `sat` is provided as float from range 0-100. Value of `color_temp` is provided as integer representing Kelvin units.", - "command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.", + "command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic. [Learn more.]({url}#command_template)", "command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)", "color_temp_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract color temperature in Kelvin from the state payload value. Expected result of the template is an integer.", "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", @@ -394,6 +406,27 @@ "transition": "Enable the transition feature for this light" } }, + "alarm_control_panel_payload_settings": { + "name": "Alarm control panel payload settings", + "data": { + "payload_arm_away": "Payload \"arm away\"", + "payload_arm_custom_bypass": "Payload \"arm custom bypass\"", + "payload_arm_disarm": "Payload \"disarm\"", + "payload_arm_home": "Payload \"arm home\"", + "payload_arm_night": "Payload \"arm night\"", + "payload_arm_vacation": "Payload \"arm vacation\"", + "payload_trigger": "Payload \"trigger alarm\"" + }, + "data_description": { + "payload_arm_away": "The payload sent when an \"arm away\" command is issued.", + "payload_arm_custom_bypass": "The payload sent when an \"arm custom bypass\" command is issued.", + "payload_arm_disarm": "The payload sent when a \"disarm\" command is issued.", + "payload_arm_home": "The payload sent when an \"arm home\" command is issued.", + "payload_arm_night": "The payload sent when an \"arm night\" command is issued.", + "payload_arm_vacation": "The payload sent when an \"arm vacation\" command is issued.", + "payload_trigger": "The payload sent when a \"trigger alarm\" command is issued." + } + }, "climate_action_settings": { "name": "Current action settings", "data": { @@ -1070,6 +1103,23 @@ } }, "selector": { + "alarm_control_panel_code_mode": { + "options": { + "local_code": "Local code validation", + "remote_code": "Numeric remote code validation", + "remote_code_text": "Text remote code validation" + } + }, + "alarm_control_panel_features": { + "options": { + "arm_away": "[%key:component::alarm_control_panel::services::alarm_arm_away::name%]", + "arm_custom_bypass": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::name%]", + "arm_home": "[%key:component::alarm_control_panel::services::alarm_arm_home::name%]", + "arm_night": "[%key:component::alarm_control_panel::services::alarm_arm_night::name%]", + "arm_vacation": "[%key:component::alarm_control_panel::services::alarm_arm_vacation::name%]", + "trigger": "[%key:component::alarm_control_panel::services::alarm_trigger::name%]" + } + }, "climate_modes": { "options": { "off": "[%key:common::state::off%]", @@ -1223,6 +1273,7 @@ }, "platform": { "options": { + "alarm_control_panel": "[%key:component::alarm_control_panel::title%]", "binary_sensor": "[%key:component::binary_sensor::title%]", "button": "[%key:component::button::title%]", "climate": "[%key:component::climate::title%]", diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index b3a93ec0cf201..417b1465aa317 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -70,6 +70,78 @@ "configuration_url": "http://example.com", } +MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_LOCAL_CODE = { + "4b06357ef8654e8d9c54cee5bb0e9391": { + "platform": "alarm_control_panel", + "name": "Alarm", + "entity_category": "config", + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code": "1234", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + "supported_features": ["arm_home", "arm_away", "trigger"], + "retain": False, + "entity_picture": "https://example.com/4b06357ef8654e8d9c54cee5bb0e9391", + }, +} +MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_REMOTE_CODE = { + "4b06357ef8654e8d9c54cee5bb0e9392": { + "platform": "alarm_control_panel", + "name": "Alarm", + "entity_category": None, + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code": "REMOTE_CODE", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + "supported_features": ["arm_home", "arm_away", "arm_custom_bypass"], + "retain": False, + "entity_picture": "https://example.com/4b06357ef8654e8d9c54cee5bb0e9392", + }, +} +MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_REMOTE_CODE_TEXT = { + "4b06357ef8654e8d9c54cee5bb0e9393": { + "platform": "alarm_control_panel", + "name": "Alarm", + "entity_category": None, + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code": "REMOTE_CODE_TEXT", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + "supported_features": ["arm_home", "arm_away", "arm_vacation"], + "retain": False, + "entity_picture": "https://example.com/4b06357ef8654e8d9c54cee5bb0e9393", + }, +} MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT = { "5b06357ef8654e8d9c54cee5bb0e939b": { "platform": "binary_sensor", @@ -444,6 +516,18 @@ "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1 | MOCK_SUBENTRY_NOTIFY_COMPONENT2, } | MOCK_SUBENTRY_AVAILABILITY_DATA +MOCK_ALARM_CONTROL_PANEL_LOCAL_CODE_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_LOCAL_CODE, +} +MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_TEXT_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, + "components": MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_REMOTE_CODE_TEXT, +} +MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, + "components": MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_REMOTE_CODE, +} MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, "components": MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 1c99d9da45fcc..b46b1557aee5c 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -33,6 +33,9 @@ from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .common import ( + MOCK_ALARM_CONTROL_PANEL_LOCAL_CODE_SUBENTRY_DATA_SINGLE, + MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_SUBENTRY_DATA_SINGLE, + MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_TEXT_SUBENTRY_DATA_SINGLE, MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_BUTTON_SUBENTRY_DATA_SINGLE, MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE, @@ -2665,6 +2668,113 @@ async def test_migrate_of_incompatible_config_entry( "entity_name", ), [ + ( + MOCK_ALARM_CONTROL_PANEL_LOCAL_CODE_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Alarm"}, + { + "entity_category": "config", + "supported_features": ["arm_home", "arm_away", "trigger"], + "alarm_control_panel_code_mode": "local_code", + }, + (), + { + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code": "1234", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "retain": False, + "alarm_control_panel_payload_settings": { + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + }, + }, + ( + ( + { + "state_topic": "test-topic", + "command_topic": "test-topic#invalid", + }, + {"command_topic": "invalid_publish_topic"}, + ), + ( + { + "command_topic": "test-topic", + "state_topic": "test-topic#invalid", + }, + {"state_topic": "invalid_subscribe_topic"}, + ), + ), + "Milk notifier Alarm", + ), + ( + MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 1}}, + {"name": "Alarm"}, + { + "supported_features": ["arm_home", "arm_away", "arm_custom_bypass"], + "alarm_control_panel_code_mode": "remote_code", + }, + (), + { + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "retain": False, + "alarm_control_panel_payload_settings": { + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + }, + }, + (), + "Milk notifier Alarm", + ), + ( + MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_TEXT_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 2}}, + {"name": "Alarm"}, + { + "supported_features": ["arm_home", "arm_away", "arm_vacation"], + "alarm_control_panel_code_mode": "remote_code_text", + }, + (), + { + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "retain": False, + "alarm_control_panel_payload_settings": { + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + }, + }, + (), + "Milk notifier Alarm", + ), ( MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 2}}, @@ -3399,6 +3509,9 @@ async def test_migrate_of_incompatible_config_entry( # MOCK_LOCK_SUBENTRY_DATA_SINGLE ], ids=[ + "alarm_control_panel_local_code", + "alarm_control_panel_remote_code", + "alarm_control_panel_remote_code_text", "binary_sensor", "button", "climate_single", @@ -3830,6 +3943,67 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites( "removed_options", ), [ + ( + ( + ConfigSubentryData( + data=MOCK_ALARM_CONTROL_PANEL_LOCAL_CODE_SUBENTRY_DATA_SINGLE, + subentry_type="device", + title="Mock subentry", + ), + ), + (), + { + "alarm_control_panel_code_mode": "remote_code", + "supported_features": ["arm_home", "arm_away", "arm_custom_bypass"], + }, + { + "command_topic": "test-topic1-updated", + "command_template": "{{ value }}", + "state_topic": "test-topic1-updated", + "value_template": "{{ value }}", + "retain": True, + }, + { + "command_topic": "test-topic1-updated", + "command_template": "{{ value }}", + "state_topic": "test-topic1-updated", + "value_template": "{{ value }}", + "retain": True, + "code": "REMOTE_CODE", + }, + {"entity_picture"}, + ), + ( + ( + ConfigSubentryData( + data=MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_SUBENTRY_DATA_SINGLE, + subentry_type="device", + title="Mock subentry", + ), + ), + (), + { + "alarm_control_panel_code_mode": "local_code", + "supported_features": ["arm_home", "arm_away", "arm_custom_bypass"], + }, + { + "command_topic": "test-topic1-updated", + "command_template": "{{ value }}", + "state_topic": "test-topic1-updated", + "value_template": "{{ value }}", + "code": "1234", + "retain": True, + }, + { + "command_topic": "test-topic1-updated", + "command_template": "{{ value }}", + "state_topic": "test-topic1-updated", + "value_template": "{{ value }}", + "code": "1234", + "retain": True, + }, + {"entity_picture"}, + ), ( ( ConfigSubentryData( @@ -4053,7 +4227,15 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites( {"entity_picture"}, ), ], - ids=["notify", "sensor", "light_basic", "climate_single", "climate_high_low"], + ids=[ + "alarm_control_panel_local_code", + "alarm_control_panel_remote_code", + "notify", + "sensor", + "light_basic", + "climate_single", + "climate_high_low", + ], ) async def test_subentry_reconfigure_edit_entity_single_entity( hass: HomeAssistant, @@ -4123,7 +4305,6 @@ async def test_subentry_reconfigure_edit_entity_single_entity( user_input={}, ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "entity_platform_config" # entity platform config flow step assert result["step_id"] == "entity_platform_config"