From 7fc8da6769c2adf08b0cb7f4d60777e9ee52fe4b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 5 Sep 2025 12:09:37 +0200 Subject: [PATCH 01/85] Add support for migrated Hue bridge (#151411) Co-authored-by: Joostlek --- homeassistant/components/hue/config_flow.py | 80 +++++++- tests/components/hue/test_config_flow.py | 215 +++++++++++++++++++- 2 files changed, 290 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index bec443526138..3328b5ab6594 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -9,7 +9,9 @@ from typing import Any import aiohttp from aiohue import LinkButtonNotPressed, create_app_key from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp +from aiohue.errors import AiohueException from aiohue.util import normalize_bridge_id +from aiohue.v2 import HueBridgeV2 import slugify as unicode_slug import voluptuous as vol @@ -40,6 +42,9 @@ HUE_MANUFACTURERURL = ("http://www.philips.com", "http://www.philips-hue.com") HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] HUE_MANUAL_BRIDGE_ID = "manual" +BSB002_MODEL_ID = "BSB002" +BSB003_MODEL_ID = "BSB003" + class HueFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Hue config flow.""" @@ -74,7 +79,14 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN): """Return a DiscoveredHueBridge object.""" try: bridge = await discover_bridge( - host, websession=aiohttp_client.async_get_clientsession(self.hass) + host, + websession=aiohttp_client.async_get_clientsession( + # NOTE: we disable SSL verification for now due to the fact that the (BSB003) + # Hue bridge uses a certificate from a on-bridge root authority. + # We need to specifically handle this case in a follow-up update. + self.hass, + verify_ssl=False, + ), ) except aiohttp.ClientError as err: LOGGER.warning( @@ -110,7 +122,9 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN): try: async with asyncio.timeout(5): bridges = await discover_nupnp( - websession=aiohttp_client.async_get_clientsession(self.hass) + websession=aiohttp_client.async_get_clientsession( + self.hass, verify_ssl=False + ) ) except TimeoutError: bridges = [] @@ -178,7 +192,9 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN): app_key = await create_app_key( bridge.host, f"home-assistant#{device_name}", - websession=aiohttp_client.async_get_clientsession(self.hass), + websession=aiohttp_client.async_get_clientsession( + self.hass, verify_ssl=False + ), ) except LinkButtonNotPressed: errors["base"] = "register_failed" @@ -228,7 +244,6 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured( updates={CONF_HOST: discovery_info.host}, reload_on_update=True ) - # we need to query the other capabilities too bridge = await self._get_bridge( discovery_info.host, discovery_info.properties["bridgeid"] @@ -236,6 +251,14 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN): if bridge is None: return self.async_abort(reason="cannot_connect") self.bridge = bridge + if ( + bridge.supports_v2 + and discovery_info.properties.get("modelid") == BSB003_MODEL_ID + ): + # try to handle migration of BSB002 --> BSB003 + if await self._check_migrated_bridge(bridge): + return self.async_abort(reason="migrated_bridge") + return await self.async_step_link() async def async_step_homekit( @@ -272,6 +295,55 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN): self.bridge = bridge return await self.async_step_link() + async def _check_migrated_bridge(self, bridge: DiscoveredHueBridge) -> bool: + """Check if the discovered bridge is a migrated bridge.""" + # Try to handle migration of BSB002 --> BSB003. + # Once we detect a BSB003 bridge on the network which has not yet been + # configured in HA (otherwise we would have had a unique id match), + # we check if we have any existing (BSB002) entries and if we can connect to the + # new bridge with our previously stored api key. + # If that succeeds, we migrate the entry to the new bridge. + for conf_entry in self.hass.config_entries.async_entries( + DOMAIN, include_ignore=False, include_disabled=False + ): + if conf_entry.data[CONF_API_VERSION] != 2: + continue + if conf_entry.data[CONF_HOST] == bridge.host: + continue + # found an existing (BSB002) bridge entry, + # check if we can connect to the new BSB003 bridge using the old credentials + api = HueBridgeV2(bridge.host, conf_entry.data[CONF_API_KEY]) + try: + await api.fetch_full_state() + except (AiohueException, aiohttp.ClientError): + continue + old_bridge_id = conf_entry.unique_id + assert old_bridge_id is not None + # found a matching entry, migrate it + self.hass.config_entries.async_update_entry( + conf_entry, + data={ + **conf_entry.data, + CONF_HOST: bridge.host, + }, + unique_id=bridge.id, + ) + # also update the bridge device + dev_reg = dr.async_get(self.hass) + if bridge_device := dev_reg.async_get_device( + identifiers={(DOMAIN, old_bridge_id)} + ): + dev_reg.async_update_device( + bridge_device.id, + # overwrite identifiers with new bridge id + new_identifiers={(DOMAIN, bridge.id)}, + # overwrite mac addresses with empty set to drop the old (incorrect) addresses + # this will be auto corrected once the integration is loaded + new_connections=set(), + ) + return True + return False + class HueV1OptionsFlowHandler(OptionsFlow): """Handle Hue options for V1 implementation.""" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index e4bdda422d1c..bc63343f9bef 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -4,7 +4,7 @@ from ipaddress import ip_address from unittest.mock import Mock, patch from aiohue.discovery import URL_NUPNP -from aiohue.errors import LinkButtonNotPressed +from aiohue.errors import AiohueException, LinkButtonNotPressed import pytest import voluptuous as vol @@ -732,3 +732,216 @@ async def test_bridge_connection_failed( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +async def test_bsb003_bridge_discovery( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a bridge being discovered.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"}, + unique_id="bsb002_00000", + ) + entry.add_to_hass(hass) + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(const.DOMAIN, "bsb002_00000")}, + connections={(dr.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF")}, + ) + create_mock_api_discovery( + aioclient_mock, + [("192.168.1.217", "bsb002_00000"), ("192.168.1.218", "bsb003_00000")], + ) + disc_bridge = get_discovered_bridge( + bridge_id="bsb003_00000", host="192.168.1.218", supports_v2=True + ) + + with ( + patch( + "homeassistant.components.hue.config_flow.discover_bridge", + return_value=disc_bridge, + ), + patch( + "homeassistant.components.hue.config_flow.HueBridgeV2", + autospec=True, + ) as mock_bridge, + ): + mock_bridge.return_value.fetch_full_state.return_value = {} + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.218"), + ip_addresses=[ip_address("192.168.1.218")], + port=443, + hostname="Philips-hue.local", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ + "bridgeid": "bsb003_00000", + "modelid": "BSB003", + }, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migrated_bridge" + + migrated_device = device_registry.async_get(device.id) + + assert migrated_device is not None + assert len(migrated_device.identifiers) == 1 + assert list(migrated_device.identifiers)[0] == (const.DOMAIN, "bsb003_00000") + # The tests don't add new connection, but that will happen + # outside of the config flow + assert len(migrated_device.connections) == 0 + assert entry.data["host"] == "192.168.1.218" + + +async def test_bsb003_bridge_discovery_old_version( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a bridge being discovered.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + data={"host": "192.168.1.217", "api_version": 1, "api_key": "abc"}, + unique_id="bsb002_00000", + ) + entry.add_to_hass(hass) + disc_bridge = get_discovered_bridge( + bridge_id="bsb003_00000", host="192.168.1.218", supports_v2=True + ) + + with patch( + "homeassistant.components.hue.config_flow.discover_bridge", + return_value=disc_bridge, + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.218"), + ip_addresses=[ip_address("192.168.1.218")], + port=443, + hostname="Philips-hue.local", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ + "bridgeid": "bsb003_00000", + "modelid": "BSB003", + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" + + +async def test_bsb003_bridge_discovery_same_host( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a bridge being discovered.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"}, + unique_id="bsb002_00000", + ) + entry.add_to_hass(hass) + create_mock_api_discovery( + aioclient_mock, + [("192.168.1.217", "bsb003_00000")], + ) + disc_bridge = get_discovered_bridge( + bridge_id="bsb003_00000", host="192.168.1.217", supports_v2=True + ) + + with ( + patch( + "homeassistant.components.hue.config_flow.discover_bridge", + return_value=disc_bridge, + ), + patch( + "homeassistant.components.hue.config_flow.HueBridgeV2", + autospec=True, + ), + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.217"), + ip_addresses=[ip_address("192.168.1.217")], + port=443, + hostname="Philips-hue.local", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ + "bridgeid": "bsb003_00000", + "modelid": "BSB003", + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" + + +@pytest.mark.parametrize("exception", [AiohueException, ClientError]) +async def test_bsb003_bridge_discovery_cannot_connect( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, + exception: Exception, +) -> None: + """Test a bridge being discovered.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"}, + unique_id="bsb002_00000", + ) + entry.add_to_hass(hass) + create_mock_api_discovery( + aioclient_mock, + [("192.168.1.217", "bsb003_00000")], + ) + disc_bridge = get_discovered_bridge( + bridge_id="bsb003_00000", host="192.168.1.217", supports_v2=True + ) + + with ( + patch( + "homeassistant.components.hue.config_flow.discover_bridge", + return_value=disc_bridge, + ), + patch( + "homeassistant.components.hue.config_flow.HueBridgeV2", + autospec=True, + ) as mock_bridge, + ): + mock_bridge.return_value.fetch_full_state.side_effect = exception + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.217"), + ip_addresses=[ip_address("192.168.1.217")], + port=443, + hostname="Philips-hue.local", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ + "bridgeid": "bsb003_00000", + "modelid": "BSB003", + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" From b6c9217429c590f63c7fd10e86a6f0d822b95846 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 4 Sep 2025 08:25:25 -0700 Subject: [PATCH 02/85] Add missing device trigger duration localizations (#151578) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- homeassistant/components/fan/strings.json | 3 +++ homeassistant/components/light/strings.json | 3 ++- homeassistant/components/remote/strings.json | 3 +++ homeassistant/components/switch/strings.json | 3 +++ homeassistant/components/update/strings.json | 3 +++ 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index c4951e88c91b..485d6aa4b59e 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -14,6 +14,9 @@ "toggle": "[%key:common::device_automation::action_type::toggle%]", "turn_on": "[%key:common::device_automation::action_type::turn_on%]", "turn_off": "[%key:common::device_automation::action_type::turn_off%]" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 7a53f2569e78..a17d6793b83e 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -57,7 +57,8 @@ }, "extra_fields": { "brightness_pct": "Brightness", - "flash": "Flash" + "flash": "Flash", + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index 09b270b96876..0c6cf98de7f3 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -14,6 +14,9 @@ "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index b73cf8f849dc..be5aa09cf342 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -14,6 +14,9 @@ "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/update/strings.json b/homeassistant/components/update/strings.json index 5194965cf69f..a90f5c8a998b 100644 --- a/homeassistant/components/update/strings.json +++ b/homeassistant/components/update/strings.json @@ -5,6 +5,9 @@ "changed_states": "{entity_name} update availability changed", "turned_on": "{entity_name} got an update available", "turned_off": "{entity_name} became up-to-date" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { From d90f2a1de140f76e3c11f24efd3c7d38f671ff92 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:23:48 +0200 Subject: [PATCH 03/85] Correct capitalization of "FRITZ!Box" in FRITZ!Box Tools integration (#151637) --- homeassistant/components/fritz/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 45d66e9621b6..5d5aba2af603 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -183,8 +183,8 @@ "description": "Sets a new password for the guest Wi-Fi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters.", "fields": { "device_id": { - "name": "Fritz!Box Device", - "description": "Select the Fritz!Box to configure." + "name": "FRITZ!Box Device", + "description": "Select the FRITZ!Box to configure." }, "password": { "name": "[%key:common::config_flow::data::password%]", From 3dacffaaf93ea0296f370c6f3c061642f7e8e67b Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Thu, 4 Sep 2025 03:33:43 -0400 Subject: [PATCH 04/85] Fix Sonos Dialog Select type conversion (#151649) --- homeassistant/components/sonos/select.py | 11 +++++++++-- homeassistant/components/sonos/speaker.py | 7 ++++++- tests/components/sonos/test_select.py | 8 ++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/select.py b/homeassistant/components/sonos/select.py index 052a1d879676..0a56e37e75c0 100644 --- a/homeassistant/components/sonos/select.py +++ b/homeassistant/components/sonos/select.py @@ -61,8 +61,15 @@ async def async_setup_entry( if ( state := getattr(speaker.soco, select_data.soco_attribute, None) ) is not None: - setattr(speaker, select_data.speaker_attribute, state) - features.append(select_data) + try: + setattr(speaker, select_data.speaker_attribute, int(state)) + features.append(select_data) + except ValueError: + _LOGGER.error( + "Invalid value for %s %s", + select_data.speaker_attribute, + state, + ) return features async def _async_create_entities(speaker: SonosSpeaker) -> None: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 427f02f04798..acf1b08cd36e 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -599,7 +599,12 @@ class SonosSpeaker: for enum_var in (ATTR_DIALOG_LEVEL,): if enum_var in variables: - setattr(self, f"{enum_var}_enum", variables[enum_var]) + try: + setattr(self, f"{enum_var}_enum", int(variables[enum_var])) + except ValueError: + _LOGGER.error( + "Invalid value for %s %s", enum_var, variables[enum_var] + ) self.async_write_entity_states() diff --git a/tests/components/sonos/test_select.py b/tests/components/sonos/test_select.py index ada48de21f39..dbbf28a52d74 100644 --- a/tests/components/sonos/test_select.py +++ b/tests/components/sonos/test_select.py @@ -38,9 +38,9 @@ async def platform_binary_sensor_fixture(): [ (0, "off"), (1, "low"), - (2, "medium"), - (3, "high"), - (4, "max"), + ("2", "medium"), + ("3", "high"), + ("4", "max"), ], ) async def test_select_dialog_level( @@ -49,7 +49,7 @@ async def test_select_dialog_level( soco, entity_registry: er.EntityRegistry, speaker_info: dict[str, str], - level: int, + level: int | str, result: str, ) -> None: """Test dialog level select entity.""" From beec6e86e0dc8018e5d77174fabb2db811699165 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Thu, 4 Sep 2025 04:30:23 -0300 Subject: [PATCH 05/85] Fix WebSocket proxy for add-ons not forwarding ping/pong frame data (#151654) --- homeassistant/components/hassio/ingress.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index e1f96b76bcb1..2938de927210 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -303,9 +303,9 @@ async def _websocket_forward( elif msg.type is aiohttp.WSMsgType.BINARY: await ws_to.send_bytes(msg.data) elif msg.type is aiohttp.WSMsgType.PING: - await ws_to.ping() + await ws_to.ping(msg.data) elif msg.type is aiohttp.WSMsgType.PONG: - await ws_to.pong() + await ws_to.pong(msg.data) elif ws_to.closed: await ws_to.close(code=ws_to.close_code, message=msg.extra) # type: ignore[arg-type] except RuntimeError: From 85b6adcc9a083d08ff67fcd403e3af457793b065 Mon Sep 17 00:00:00 2001 From: Imeon-Energy Date: Fri, 5 Sep 2025 12:11:44 +0200 Subject: [PATCH 06/85] Fix, entities stay unavailable after timeout error, Imeon inverter integration (#151671) Co-authored-by: TheBushBoy --- homeassistant/components/imeon_inverter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imeon_inverter/manifest.json b/homeassistant/components/imeon_inverter/manifest.json index a9a37f3fd9c7..837b73512417 100644 --- a/homeassistant/components/imeon_inverter/manifest.json +++ b/homeassistant/components/imeon_inverter/manifest.json @@ -7,7 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["imeon_inverter_api==0.3.14"], + "requirements": ["imeon_inverter_api==0.3.16"], "ssdp": [ { "manufacturer": "IMEON", diff --git a/requirements_all.txt b/requirements_all.txt index 37e728130d76..9e5ff5a2ab9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ igloohome-api==0.1.1 ihcsdk==2.8.5 # homeassistant.components.imeon_inverter -imeon_inverter_api==0.3.14 +imeon_inverter_api==0.3.16 # homeassistant.components.imgw_pib imgw_pib==1.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a294ba2d468e..f7144f8bf10a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1075,7 +1075,7 @@ ifaddr==0.2.0 igloohome-api==0.1.1 # homeassistant.components.imeon_inverter -imeon_inverter_api==0.3.14 +imeon_inverter_api==0.3.16 # homeassistant.components.imgw_pib imgw_pib==1.5.4 From 8710267d53215d5629dab37e3a82e13699eac525 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 4 Sep 2025 14:44:10 +0200 Subject: [PATCH 07/85] Bump aiohue to 4.7.5 (#151684) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 8bc3d84bd503..e6f431727d01 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -10,6 +10,6 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["aiohue"], - "requirements": ["aiohue==4.7.4"], + "requirements": ["aiohue==4.7.5"], "zeroconf": ["_hue._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9e5ff5a2ab9e..2a2a262f6b31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -277,7 +277,7 @@ aiohomekit==3.2.15 aiohttp_sse==2.2.0 # homeassistant.components.hue -aiohue==4.7.4 +aiohue==4.7.5 # homeassistant.components.imap aioimaplib==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7144f8bf10a..d2ee0038e50f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -262,7 +262,7 @@ aiohomekit==3.2.15 aiohttp_sse==2.2.0 # homeassistant.components.hue -aiohue==4.7.4 +aiohue==4.7.5 # homeassistant.components.imap aioimaplib==2.0.1 From fcc3f92f8c7b7501f0bc3523a44b5d454eb7392c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 4 Sep 2025 17:04:49 +0200 Subject: [PATCH 08/85] Update frontend to 20250903.3 (#151694) --- 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 becab5a18c51..d74bf1f30b7f 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==20250903.2"] + "requirements": ["home-assistant-frontend==20250903.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 50acadce8088..b43da76a609f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.3.0 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250903.2 +home-assistant-frontend==20250903.3 home-assistant-intents==2025.9.3 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2a2a262f6b31..395da3579d68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.2 +home-assistant-frontend==20250903.3 # homeassistant.components.conversation home-assistant-intents==2025.9.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2ee0038e50f..bf3f71baa7d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.2 +home-assistant-frontend==20250903.3 # homeassistant.components.conversation home-assistant-intents==2025.9.3 From bfdd2053baeb4b65fca913aa7bd3a0fdc306f413 Mon Sep 17 00:00:00 2001 From: Dan Raper Date: Fri, 5 Sep 2025 06:37:40 +0100 Subject: [PATCH 09/85] Require OhmeAdvancedSettingsCoordinator to run regardless of entities (#151701) --- homeassistant/components/ohme/coordinator.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ohme/coordinator.py b/homeassistant/components/ohme/coordinator.py index 864b03e9a7c1..d9e009ed1f10 100644 --- a/homeassistant/components/ohme/coordinator.py +++ b/homeassistant/components/ohme/coordinator.py @@ -10,7 +10,7 @@ import logging from ohme import ApiException, OhmeApiClient from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -83,6 +83,21 @@ class OhmeAdvancedSettingsCoordinator(OhmeBaseCoordinator): coordinator_name = "Advanced Settings" + def __init__( + self, hass: HomeAssistant, config_entry: OhmeConfigEntry, client: OhmeApiClient + ) -> None: + """Initialise coordinator.""" + super().__init__(hass, config_entry, client) + + @callback + def _dummy_listener() -> None: + pass + + # This coordinator is used by the API library to determine whether the + # charger is online and available. It is therefore required even if no + # entities are using it. + self.async_add_listener(_dummy_listener) + async def _internal_update_data(self) -> None: """Fetch data from API endpoint.""" await self.client.async_get_advanced_settings() From 7037ce989c847c655aad9910a158b05932dbd210 Mon Sep 17 00:00:00 2001 From: Dan Raper Date: Fri, 5 Sep 2025 03:23:57 +0100 Subject: [PATCH 10/85] Bump ohmepy version to 1.5.2 (#151707) --- homeassistant/components/ohme/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ohme/manifest.json b/homeassistant/components/ohme/manifest.json index 786c615d68a3..14612fff6eb8 100644 --- a/homeassistant/components/ohme/manifest.json +++ b/homeassistant/components/ohme/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["ohme==1.5.1"] + "requirements": ["ohme==1.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 395da3579d68..d34c85433a48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1580,7 +1580,7 @@ odp-amsterdam==6.1.2 oemthermostat==1.1.1 # homeassistant.components.ohme -ohme==1.5.1 +ohme==1.5.2 # homeassistant.components.ollama ollama==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf3f71baa7d7..bc67e29c2aa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1348,7 +1348,7 @@ objgraph==3.5.0 odp-amsterdam==6.1.2 # homeassistant.components.ohme -ohme==1.5.1 +ohme==1.5.2 # homeassistant.components.ollama ollama==0.5.1 From 2bb4573357189a479dba36a4a230884cefbdbcab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 5 Sep 2025 04:23:08 +0200 Subject: [PATCH 11/85] Update Mill library 0.13.1 (#151712) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index c5cc94ead305..4ae2ac8bbbfb 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.12.5", "mill-local==0.3.0"] + "requirements": ["millheater==0.13.1", "mill-local==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d34c85433a48..2addc42cd787 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1441,7 +1441,7 @@ microBeesPy==0.3.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.5 +millheater==0.13.1 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc67e29c2aa3..9b2562e5727a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1233,7 +1233,7 @@ microBeesPy==0.3.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.5 +millheater==0.13.1 # homeassistant.components.minio minio==7.1.12 From 89c335919ac3d411482d9c47c4057f4fc1ff9135 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 5 Sep 2025 03:28:37 -0500 Subject: [PATCH 12/85] Handle match failures in intent HTTP API (#151726) --- homeassistant/components/intent/__init__.py | 2 +- tests/components/intent/test_init.py | 26 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 72853276ab3a..17ec8602d983 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -615,7 +615,7 @@ class IntentHandleView(http.HomeAssistantView): intent_result = await intent.async_handle( hass, DOMAIN, intent_name, slots, "", self.context(request) ) - except intent.IntentHandleError as err: + except (intent.IntentHandleError, intent.MatchFailedError) as err: intent_result = intent.IntentResponse(language=language) intent_result.async_set_speech(str(err)) diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 3779930e360a..1993ebe46e40 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -73,6 +73,32 @@ async def test_http_handle_intent( } +async def test_http_handle_intent_match_failure( + hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser +) -> None: + """Test handle intent match failure via HTTP API.""" + + assert await async_setup_component(hass, "intent", {}) + + hass.states.async_set( + "cover.garage_door_1", "closed", {ATTR_FRIENDLY_NAME: "Garage Door"} + ) + hass.states.async_set( + "cover.garage_door_2", "closed", {ATTR_FRIENDLY_NAME: "Garage Door"} + ) + async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + + client = await hass_client() + resp = await client.post( + "/api/intent/handle", + json={"name": "HassTurnOn", "data": {"name": "Garage Door"}}, + ) + assert resp.status == 200 + data = await resp.json() + + assert "DUPLICATE_NAME" in data["speech"]["plain"]["speech"] + + async def test_cover_intents_loading(hass: HomeAssistant) -> None: """Test Cover Intents Loading.""" assert await async_setup_component(hass, "intent", {}) From dff3d5f8affd4fd5598a096410c450278cae577e Mon Sep 17 00:00:00 2001 From: David Knowles Date: Thu, 4 Sep 2025 22:21:09 -0400 Subject: [PATCH 13/85] Bump pyschlage to 2025.9.0 (#151731) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index b71afe01e560..eadf5585f307 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2025.7.3"] + "requirements": ["pyschlage==2025.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2addc42cd787..16b1504d7688 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2312,7 +2312,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2025.7.3 +pyschlage==2025.9.0 # homeassistant.components.sensibo pysensibo==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b2562e5727a..e5394b0c0747 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ pyrympro==0.0.9 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2025.7.3 +pyschlage==2025.9.0 # homeassistant.components.sensibo pysensibo==1.2.1 From 7dbeaa475d45221ac66fadbb4d65d41d67336001 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:37:51 +0200 Subject: [PATCH 14/85] Bump bimmer_connected to 0.17.3 (#151756) --- .../bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_diagnostics.ambr | 79 +++++++++++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 81928a59a52b..327b47bbea29 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected[china]==0.17.2"] + "requirements": ["bimmer-connected[china]==0.17.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 16b1504d7688..88442d3864a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -618,7 +618,7 @@ beautifulsoup4==4.13.3 # beewi-smartclim==0.0.10 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.17.2 +bimmer-connected[china]==0.17.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5394b0c0747..046d9b3bfcb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -555,7 +555,7 @@ base36==0.1.1 beautifulsoup4==4.13.3 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.17.2 +bimmer-connected[china]==0.17.3 # homeassistant.components.eq3btsmart # homeassistant.components.esphome diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index b87da22a332e..06e90c878af2 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -138,6 +138,7 @@ 'state': 'LOW', }), ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'INACTIVE', @@ -193,6 +194,24 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), + 'next_service_by_time': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ @@ -1053,6 +1072,7 @@ 'state': 'LOW', }), ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'HEATING', @@ -1108,6 +1128,24 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), + 'next_service_by_time': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ @@ -1858,6 +1896,7 @@ 'state': 'LOW', }), ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'INACTIVE', @@ -1922,6 +1961,24 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), + 'next_service_by_time': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ @@ -2621,6 +2678,7 @@ 'has_check_control_messages': False, 'messages': list([ ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'UNKNOWN', @@ -2658,6 +2716,16 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': None, + 'next_service_by_time': dict({ + 'due_date': '2022-10-01T00:00:00+00:00', + 'due_distance': list([ + None, + None, + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ @@ -4991,6 +5059,7 @@ 'has_check_control_messages': False, 'messages': list([ ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'UNKNOWN', @@ -5028,6 +5097,16 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': None, + 'next_service_by_time': dict({ + 'due_date': '2022-10-01T00:00:00+00:00', + 'due_distance': list([ + None, + None, + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ From 625f586945dabd43a41e1535e69bdaa39a039653 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:42:56 +0200 Subject: [PATCH 15/85] Fix recognition of entity names in default agent with interpunction (#151759) --- .../components/conversation/default_agent.py | 10 ++++---- .../conversation/test_default_agent.py | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 4b056ead2c29..938889955e96 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -35,7 +35,7 @@ from hassil.recognize import ( ) from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity from hassil.trie import Trie -from hassil.util import merge_dict +from hassil.util import merge_dict, remove_punctuation from home_assistant_intents import ( ErrorKey, FuzzyConfig, @@ -327,12 +327,10 @@ class DefaultAgent(ConversationEntity): if self._exposed_names_trie is not None: # Filter by input string - text_lower = user_input.text.strip().lower() + text = remove_punctuation(user_input.text).strip().lower() slot_lists["name"] = TextSlotList( name="name", - values=[ - result[2] for result in self._exposed_names_trie.find(text_lower) - ], + values=[result[2] for result in self._exposed_names_trie.find(text)], ) start = time.monotonic() @@ -1263,7 +1261,7 @@ class DefaultAgent(ConversationEntity): name_list = TextSlotList.from_tuples(exposed_entity_names, allow_template=False) for name_value in name_list.values: assert isinstance(name_value.text_in, TextChunk) - name_text = name_value.text_in.text.strip().lower() + name_text = remove_punctuation(name_value.text_in.text).strip().lower() self._exposed_names_trie.insert(name_text, name_value) self._slot_lists = { diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 7c5e897d86c6..a90cd1b55c18 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -231,6 +231,29 @@ async def test_conversation_agent(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("init_components") +async def test_punctuation(hass: HomeAssistant) -> None: + """Test punctuation is handled properly.""" + hass.states.async_set( + "light.test_light", + "off", + attributes={ATTR_FRIENDLY_NAME: "Test light"}, + ) + expose_entity(hass, "light.test_light", True) + + calls = async_mock_service(hass, "light", "turn_on") + result = await conversation.async_converse( + hass, "Turn?? on,, test;; light!!!", None, Context(), None + ) + + assert len(calls) == 1 + assert calls[0].data["entity_id"][0] == "light.test_light" + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["name"]["value"] == "test light" + assert result.response.intent.slots["name"]["text"] == "test light" + + async def test_expose_flag_automatically_set( hass: HomeAssistant, entity_registry: er.EntityRegistry, From 06480bfd9d7b0978948cb4c6c342a1feed5d68d3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 5 Sep 2025 12:31:33 +0200 Subject: [PATCH 16/85] Fix enable/disable entity in modbus (#151626) --- homeassistant/components/modbus/entity.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 38622c4c1974..8667bc17a796 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -62,7 +62,6 @@ from .const import ( CONF_VIRTUAL_COUNT, CONF_WRITE_TYPE, CONF_ZERO_SUPPRESS, - SIGNAL_START_ENTITY, SIGNAL_STOP_ENTITY, DataType, ) @@ -143,7 +142,6 @@ class BasePlatform(Entity): self._cancel_call() self._cancel_call = None self._attr_available = False - self.async_write_ha_state() async def async_await_connection(self, _now: Any) -> None: """Wait for first connect.""" @@ -162,11 +160,6 @@ class BasePlatform(Entity): self.async_on_remove( async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_disable) ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_START_ENTITY, self.async_local_update - ) - ) class BaseStructPlatform(BasePlatform, RestoreEntity): From ae58e633f0775152909a67564f4fcf7d76c75dee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Sep 2025 10:33:36 +0000 Subject: [PATCH 17/85] Bump version to 2025.9.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 d46b4cd7717f..318594196e27 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -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 45751ec957d3..ee06c96403bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0" +version = "2025.9.1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 9a43f2776dc350b6b56d9d848f965957e3ce106b Mon Sep 17 00:00:00 2001 From: Bob Igo Date: Tue, 9 Sep 2025 09:18:48 -0400 Subject: [PATCH 18/85] Fix XMPP not working with non-TLS servers (#150957) --- homeassistant/components/xmpp/notify.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index c9829746d59a..ee57abd769d5 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -146,6 +146,8 @@ async def async_send_message( # noqa: C901 self.enable_starttls = use_tls self.enable_direct_tls = use_tls + self.enable_plaintext = not use_tls + self["feature_mechanisms"].unencrypted_scram = not use_tls self.use_ipv6 = False self.add_event_handler("failed_all_auth", self.disconnect_on_login_fail) self.add_event_handler("session_start", self.start) From ca79f4c963a8f8f53616a46be9c897d0213481ae Mon Sep 17 00:00:00 2001 From: Mark Adkins Date: Fri, 5 Sep 2025 07:14:28 -0400 Subject: [PATCH 19/85] Update SharkIQ authentication method (#151046) --- homeassistant/components/sharkiq/__init__.py | 12 +++++++++--- homeassistant/components/sharkiq/config_flow.py | 10 +++++++--- homeassistant/components/sharkiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sharkiq/test_config_flow.py | 16 +++++++++++++--- tests/components/sharkiq/test_vacuum.py | 3 +++ 7 files changed, 35 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index e560bb77b578..b87f52ba7b11 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -3,6 +3,7 @@ import asyncio from contextlib import suppress +import aiohttp from sharkiq import ( AylaApi, SharkIqAuthError, @@ -15,7 +16,7 @@ from homeassistant import exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( API_TIMEOUT, @@ -56,10 +57,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b data={**config_entry.data, CONF_REGION: SHARKIQ_REGION_DEFAULT}, ) + new_websession = async_create_clientsession( + hass, + cookie_jar=aiohttp.CookieJar(unsafe=True, quote_cookie=False), + ) + ayla_api = get_ayla_api( username=config_entry.data[CONF_USERNAME], password=config_entry.data[CONF_PASSWORD], - websession=async_get_clientsession(hass), + websession=new_websession, europe=(config_entry.data[CONF_REGION] == SHARKIQ_REGION_EUROPE), ) @@ -94,7 +100,7 @@ async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator): await coordinator.ayla_api.async_sign_out() -async def async_update_options(hass, config_entry): +async def async_update_options(hass: HomeAssistant, config_entry): """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 87367fcf093e..7174c6347879 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import selector -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( DOMAIN, @@ -44,15 +44,19 @@ async def _validate_input( hass: HomeAssistant, data: Mapping[str, Any] ) -> dict[str, str]: """Validate the user input allows us to connect.""" + new_websession = async_create_clientsession( + hass, + cookie_jar=aiohttp.CookieJar(unsafe=True, quote_cookie=False), + ) ayla_api = get_ayla_api( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], - websession=async_get_clientsession(hass), + websession=new_websession, europe=(data[CONF_REGION] == SHARKIQ_REGION_EUROPE), ) try: - async with asyncio.timeout(10): + async with asyncio.timeout(15): LOGGER.debug("Initialize connection to Ayla networks API") await ayla_api.async_sign_in() except (TimeoutError, aiohttp.ClientError, TypeError) as error: diff --git a/homeassistant/components/sharkiq/manifest.json b/homeassistant/components/sharkiq/manifest.json index c29fc582462c..793f65483ea2 100644 --- a/homeassistant/components/sharkiq/manifest.json +++ b/homeassistant/components/sharkiq/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sharkiq", "iot_class": "cloud_polling", "loggers": ["sharkiq"], - "requirements": ["sharkiq==1.1.1"] + "requirements": ["sharkiq==1.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88442d3864a4..c6b1483fcda2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2768,7 +2768,7 @@ sentry-sdk==1.45.1 sfrbox-api==0.0.12 # homeassistant.components.sharkiq -sharkiq==1.1.1 +sharkiq==1.4.0 # homeassistant.components.aquostv sharp_aquos_rc==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 046d9b3bfcb0..43803a1861be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2290,7 +2290,7 @@ sentry-sdk==1.45.1 sfrbox-api==0.0.12 # homeassistant.components.sharkiq -sharkiq==1.1.1 +sharkiq==1.4.0 # homeassistant.components.simplefin simplefin4py==0.0.18 diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index 22a77678c0d7..f96b2f31e0b1 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -47,6 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: with ( patch("sharkiq.AylaApi.async_sign_in", return_value=True), + patch("sharkiq.AylaApi.async_set_cookie"), patch( "homeassistant.components.sharkiq.async_setup_entry", return_value=True, @@ -84,7 +85,10 @@ async def test_form_error(hass: HomeAssistant, exc: Exception, base_error: str) DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch.object(AylaApi, "async_sign_in", side_effect=exc): + with ( + patch.object(AylaApi, "async_sign_in", side_effect=exc), + patch("sharkiq.AylaApi.async_set_cookie"), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, @@ -101,7 +105,10 @@ async def test_reauth_success(hass: HomeAssistant) -> None: result = await mock_config.start_reauth_flow(hass) - with patch("sharkiq.AylaApi.async_sign_in", return_value=True): + with ( + patch("sharkiq.AylaApi.async_sign_in", return_value=True), + patch("sharkiq.AylaApi.async_set_cookie"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG ) @@ -132,7 +139,10 @@ async def test_reauth( result = await mock_config.start_reauth_flow(hass) - with patch("sharkiq.AylaApi.async_sign_in", side_effect=side_effect): + with ( + patch("sharkiq.AylaApi.async_sign_in", side_effect=side_effect), + patch("sharkiq.AylaApi.async_set_cookie"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG ) diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index bfb2176026b7..5b5339ec7a21 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -80,6 +80,9 @@ class MockAyla(AylaApi): async def async_sign_in(self): """Instead of signing in, just return.""" + async def async_set_cookie(self): + """Instead of getting cookies, just return.""" + async def async_refresh_auth(self): """Instead of refreshing auth, just return.""" From 0b56ec16edc5b8828f1f2c01af17fc495ef5b03b Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 12 Sep 2025 11:52:29 +0200 Subject: [PATCH 20/85] Add event entity on websocket ready in Husqvarna Automower (#151428) --- .../components/husqvarna_automower/event.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/event.py b/homeassistant/components/husqvarna_automower/event.py index 7fe8bae8c2de..2d7edcf1c73f 100644 --- a/homeassistant/components/husqvarna_automower/event.py +++ b/homeassistant/components/husqvarna_automower/event.py @@ -36,12 +36,13 @@ async def async_setup_entry( """Set up Automower message event entities. Entities are created dynamically based on messages received from the API, - but only for mowers that support message events. + but only for mowers that support message events after the WebSocket connection + is ready. """ coordinator = config_entry.runtime_data entity_registry = er.async_get(hass) - restored_mowers = { + restored_mowers: set[str] = { entry.unique_id.removesuffix("_message") for entry in er.async_entries_for_config_entry( entity_registry, config_entry.entry_id @@ -49,14 +50,20 @@ async def async_setup_entry( if entry.domain == EVENT_DOMAIN } - async_add_entities( - AutomowerMessageEventEntity(mower_id, coordinator) - for mower_id in restored_mowers - if mower_id in coordinator.data - ) + @callback + def _on_ws_ready() -> None: + async_add_entities( + AutomowerMessageEventEntity(mower_id, coordinator, websocket_alive=True) + for mower_id in restored_mowers + if mower_id in coordinator.data + ) + coordinator.api.unregister_ws_ready_callback(_on_ws_ready) + + coordinator.api.register_ws_ready_callback(_on_ws_ready) @callback def _handle_message(msg: SingleMessageData) -> None: + """Add entity dynamically if a new mower sends messages.""" if msg.id in restored_mowers: return @@ -78,11 +85,17 @@ class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity): self, mower_id: str, coordinator: AutomowerDataUpdateCoordinator, + *, + websocket_alive: bool | None = None, ) -> None: """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 + self.websocket_alive: bool = ( + websocket_alive + if websocket_alive is not None + else coordinator.websocket_alive + ) @property def available(self) -> bool: From fe01e9601240955ec4036599a37cc22d5f0d80f1 Mon Sep 17 00:00:00 2001 From: hbludworth <63749412+hbludworth@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:16:37 -0600 Subject: [PATCH 21/85] Fix Aladdin Connect state not updating (#151652) Co-authored-by: Joostlek --- .../components/aladdin_connect/__init__.py | 18 +----------------- .../components/aladdin_connect/coordinator.py | 6 ++++++ .../components/aladdin_connect/cover.py | 4 +++- .../components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/aladdin_connect/test_init.py | 2 ++ 7 files changed, 15 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index adcc53bfc75c..48bedafdd1ab 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from genie_partner_sdk.client import AladdinConnectClient -from genie_partner_sdk.model import GarageDoor from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -36,22 +35,7 @@ async def async_setup_entry( api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session) ) - sdk_doors = await client.get_doors() - - # Convert SDK GarageDoor objects to integration GarageDoor objects - doors = [ - GarageDoor( - { - "device_id": door.device_id, - "door_number": door.door_number, - "name": door.name, - "status": door.status, - "link_status": door.link_status, - "battery_level": door.battery_level, - } - ) - for door in sdk_doors - ] + doors = await client.get_doors() entry.runtime_data = { door.unique_id: AladdinConnectCoordinator(hass, entry, client, door) diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py index 74afbe8fca97..718aed8e4457 100644 --- a/homeassistant/components/aladdin_connect/coordinator.py +++ b/homeassistant/components/aladdin_connect/coordinator.py @@ -41,4 +41,10 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]): async def _async_update_data(self) -> GarageDoor: """Fetch data from the Aladdin Connect API.""" await self.client.update_door(self.data.device_id, self.data.door_number) + self.data.status = self.client.get_door_status( + self.data.device_id, self.data.door_number + ) + self.data.battery_level = self.client.get_battery_status( + self.data.device_id, self.data.door_number + ) return self.data diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 7af0e4eb2cee..4bc787539fd9 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -49,7 +49,9 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity): @property def is_closed(self) -> bool | None: """Update is closed attribute.""" - return self.coordinator.data.status == "closed" + if (status := self.coordinator.data.status) is None: + return None + return status == "closed" @property def is_closing(self) -> bool | None: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 67c755e29a8e..3b192d17b987 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["genie-partner-sdk==1.0.10"] + "requirements": ["genie-partner-sdk==1.0.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index c6b1483fcda2..9beabcb8d8c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1002,7 +1002,7 @@ gassist-text==0.0.14 gcal-sync==8.0.0 # homeassistant.components.aladdin_connect -genie-partner-sdk==1.0.10 +genie-partner-sdk==1.0.11 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43803a1861be..8b3a6caa9752 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -872,7 +872,7 @@ gassist-text==0.0.14 gcal-sync==8.0.0 # homeassistant.components.aladdin_connect -genie-partner-sdk==1.0.10 +genie-partner-sdk==1.0.11 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index e26e5234f1c2..bc147839c2fe 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -30,6 +30,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: mock_door.status = "closed" mock_door.link_status = "connected" mock_door.battery_level = 100 + mock_door.unique_id = f"{mock_door.device_id}-{mock_door.door_number}" mock_client = AsyncMock() mock_client.get_doors.return_value = [mock_door] @@ -80,6 +81,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: mock_door.status = "closed" mock_door.link_status = "connected" mock_door.battery_level = 100 + mock_door.unique_id = f"{mock_door.device_id}-{mock_door.door_number}" # Mock client mock_client = AsyncMock() From 36b3133fa2afeaf495a9c7c068a65bc21bfe88bd Mon Sep 17 00:00:00 2001 From: blotus Date: Fri, 5 Sep 2025 14:40:14 +0200 Subject: [PATCH 22/85] Fix support for Ecowitt soil moisture sensors (#151685) --- homeassistant/components/ecowitt/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index ccaaeaae3de3..6620f61961ff 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -218,6 +218,12 @@ ECOWITT_SENSORS_MAPPING: Final = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + EcoWittSensorTypes.SOIL_MOISTURE: SensorEntityDescription( + key="SOIL_MOISTURE", + device_class=SensorDeviceClass.MOISTURE, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), } From b387acffb76744d6ad5c8011f573cc946e0fffaa Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 8 Sep 2025 12:48:23 +0200 Subject: [PATCH 23/85] Fix update of the entity ID does not clean up an old restored state (#151696) Co-authored-by: Erik Montnemery --- homeassistant/helpers/entity_registry.py | 16 +++++++- tests/helpers/test_entity_registry.py | 51 +++++++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index f1a765b3ddcc..ae3da68bd74b 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1898,11 +1898,25 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - @callback def cleanup_restored_states_filter(event_data: Mapping[str, Any]) -> bool: """Clean up restored states filter.""" - return bool(event_data["action"] == "remove") + return (event_data["action"] == "remove") or ( + event_data["action"] == "update" + and "old_entity_id" in event_data + and event_data["entity_id"] != event_data["old_entity_id"] + ) @callback def cleanup_restored_states(event: Event[EventEntityRegistryUpdatedData]) -> None: """Clean up restored states.""" + if event.data["action"] == "update": + old_entity_id = event.data["old_entity_id"] + old_state = hass.states.get(old_entity_id) + if old_state is None or not old_state.attributes.get(ATTR_RESTORED): + return + hass.states.async_remove(old_entity_id, context=event.context) + if entry := registry.async_get(event.data["entity_id"]): + entry.write_unavailable_state(hass) + return + state = hass.states.get(event.data["entity_id"]) if state is None or not state.attributes.get(ATTR_RESTORED): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 421f52bca737..593e1ea97032 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1462,9 +1462,56 @@ async def test_update_entity_unique_id_conflict( ) -async def test_update_entity_entity_id(entity_registry: er.EntityRegistry) -> None: - """Test entity's entity_id is updated.""" +async def test_update_entity_entity_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test entity's entity_id is updated for entity with a restored state.""" + hass.set_state(CoreState.not_running) + + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) + entry = entity_registry.async_get_or_create( + "light", "hue", "5678", config_entry=mock_config + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + assert ( + entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id + ) + state = hass.states.get(entry.entity_id) + assert state is not None + assert state.state == "unavailable" + assert state.attributes == {"restored": True, "supported_features": 0} + + new_entity_id = "light.blah" + assert new_entity_id != entry.entity_id + with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save: + updated_entry = entity_registry.async_update_entity( + entry.entity_id, new_entity_id=new_entity_id + ) + assert updated_entry != entry + assert updated_entry.entity_id == new_entity_id + assert mock_schedule_save.call_count == 1 + + assert entity_registry.async_get(entry.entity_id) is None + assert entity_registry.async_get(new_entity_id) is not None + + # The restored state should be removed + old_state = hass.states.get(entry.entity_id) + assert old_state is None + + # The new entity should have an unavailable initial state + new_state = hass.states.get(new_entity_id) + assert new_state is not None + assert new_state.state == "unavailable" + + +async def test_update_entity_entity_id_without_state( + entity_registry: er.EntityRegistry, +) -> None: + """Test entity's entity_id is updated for entity without a state.""" entry = entity_registry.async_get_or_create("light", "hue", "5678") + assert ( entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id ) From 0091dafcb04b8fc0bba0a2aa32a7386dc88a90ec Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 5 Sep 2025 18:47:29 +0300 Subject: [PATCH 24/85] Revert "Jewish Calendar add coordinator " (#151780) --- .../components/jewish_calendar/__init__.py | 18 +-- .../jewish_calendar/binary_sensor.py | 3 +- .../components/jewish_calendar/coordinator.py | 116 -------------- .../components/jewish_calendar/diagnostics.py | 2 +- .../components/jewish_calendar/entity.py | 64 +++++++- .../components/jewish_calendar/sensor.py | 36 +++-- .../snapshots/test_diagnostics.ambr | 150 +++++++++--------- 7 files changed, 163 insertions(+), 226 deletions(-) delete mode 100644 homeassistant/components/jewish_calendar/coordinator.py diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 0f5a066600c8..8e01b6b6ae0b 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -29,8 +29,7 @@ from .const import ( DEFAULT_LANGUAGE, DOMAIN, ) -from .coordinator import JewishCalendarData, JewishCalendarUpdateCoordinator -from .entity import JewishCalendarConfigEntry +from .entity import JewishCalendarConfigEntry, JewishCalendarData from .services import async_setup_services _LOGGER = logging.getLogger(__name__) @@ -70,7 +69,7 @@ async def async_setup_entry( ) ) - data = JewishCalendarData( + config_entry.runtime_data = JewishCalendarData( language, diaspora, location, @@ -78,11 +77,8 @@ async def async_setup_entry( havdalah_offset, ) - coordinator = JewishCalendarUpdateCoordinator(hass, config_entry, data) - await coordinator.async_config_entry_first_refresh() - - config_entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + return True @@ -90,13 +86,7 @@ async def async_unload_entry( hass: HomeAssistant, config_entry: JewishCalendarConfigEntry ) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ): - coordinator = config_entry.runtime_data - if coordinator.event_unsub: - coordinator.event_unsub() - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) async def async_migrate_entry( diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 205691bc1838..d5097df962f5 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -72,7 +72,8 @@ class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - return self.entity_description.is_on(self.coordinator.zmanim)(dt_util.now()) + zmanim = self.make_zmanim(dt.date.today()) + return self.entity_description.is_on(zmanim)(dt_util.now()) def _update_times(self, zmanim: Zmanim) -> list[dt.datetime | None]: """Return a list of times to update the sensor.""" diff --git a/homeassistant/components/jewish_calendar/coordinator.py b/homeassistant/components/jewish_calendar/coordinator.py deleted file mode 100644 index 21713313043e..000000000000 --- a/homeassistant/components/jewish_calendar/coordinator.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Data update coordinator for Jewish calendar.""" - -from dataclasses import dataclass -import datetime as dt -import logging - -from hdate import HDateInfo, Location, Zmanim -from hdate.translator import Language, set_language - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers import event -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -import homeassistant.util.dt as dt_util - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarUpdateCoordinator] - - -@dataclass -class JewishCalendarData: - """Jewish Calendar runtime dataclass.""" - - language: Language - diaspora: bool - location: Location - candle_lighting_offset: int - havdalah_offset: int - dateinfo: HDateInfo | None = None - zmanim: Zmanim | None = None - - -class JewishCalendarUpdateCoordinator(DataUpdateCoordinator[JewishCalendarData]): - """Data update coordinator class for Jewish calendar.""" - - config_entry: JewishCalendarConfigEntry - event_unsub: CALLBACK_TYPE | None = None - - def __init__( - self, - hass: HomeAssistant, - config_entry: JewishCalendarConfigEntry, - data: JewishCalendarData, - ) -> None: - """Initialize the coordinator.""" - super().__init__(hass, _LOGGER, name=DOMAIN, config_entry=config_entry) - self.data = data - self._unsub_update: CALLBACK_TYPE | None = None - set_language(data.language) - - async def _async_update_data(self) -> JewishCalendarData: - """Return HDate and Zmanim for today.""" - now = dt_util.now() - _LOGGER.debug("Now: %s Location: %r", now, self.data.location) - - today = now.date() - - self.data.dateinfo = HDateInfo(today, self.data.diaspora) - self.data.zmanim = self.make_zmanim(today) - self.async_schedule_future_update() - return self.data - - @callback - def async_schedule_future_update(self) -> None: - """Schedule the next update of the sensor for the upcoming midnight.""" - # Cancel any existing update - if self._unsub_update: - self._unsub_update() - self._unsub_update = None - - # Calculate the next midnight - next_midnight = dt_util.start_of_local_day() + dt.timedelta(days=1) - - _LOGGER.debug("Scheduling next update at %s", next_midnight) - - # Schedule update at next midnight - self._unsub_update = event.async_track_point_in_time( - self.hass, self._handle_midnight_update, next_midnight - ) - - @callback - def _handle_midnight_update(self, _now: dt.datetime) -> None: - """Handle midnight update callback.""" - self._unsub_update = None - self.async_set_updated_data(self.data) - - async def async_shutdown(self) -> None: - """Cancel any scheduled updates when the coordinator is shutting down.""" - await super().async_shutdown() - if self._unsub_update: - self._unsub_update() - self._unsub_update = None - - def make_zmanim(self, date: dt.date) -> Zmanim: - """Create a Zmanim object.""" - return Zmanim( - date=date, - location=self.data.location, - candle_lighting_offset=self.data.candle_lighting_offset, - havdalah_offset=self.data.havdalah_offset, - ) - - @property - def zmanim(self) -> Zmanim: - """Return the current Zmanim.""" - assert self.data.zmanim is not None, "Zmanim data not available" - return self.data.zmanim - - @property - def dateinfo(self) -> HDateInfo: - """Return the current HDateInfo.""" - assert self.data.dateinfo is not None, "HDateInfo data not available" - return self.data.dateinfo diff --git a/homeassistant/components/jewish_calendar/diagnostics.py b/homeassistant/components/jewish_calendar/diagnostics.py index f2db0786b128..27415282b6df 100644 --- a/homeassistant/components/jewish_calendar/diagnostics.py +++ b/homeassistant/components/jewish_calendar/diagnostics.py @@ -24,5 +24,5 @@ async def async_get_config_entry_diagnostics( return { "entry_data": async_redact_data(entry.data, TO_REDACT), - "data": async_redact_data(asdict(entry.runtime_data.data), TO_REDACT), + "data": async_redact_data(asdict(entry.runtime_data), TO_REDACT), } diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py index d3007212739b..d5e411290757 100644 --- a/homeassistant/components/jewish_calendar/entity.py +++ b/homeassistant/components/jewish_calendar/entity.py @@ -1,22 +1,48 @@ """Entity representing a Jewish Calendar sensor.""" from abc import abstractmethod +from dataclasses import dataclass import datetime as dt +import logging -from hdate import Zmanim +from hdate import HDateInfo, Location, Zmanim +from hdate.translator import Language, set_language +from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import event from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.entity import EntityDescription -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.util import dt as dt_util from .const import DOMAIN -from .coordinator import JewishCalendarConfigEntry, JewishCalendarUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarData] -class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]): +@dataclass +class JewishCalendarDataResults: + """Jewish Calendar results dataclass.""" + + dateinfo: HDateInfo + zmanim: Zmanim + + +@dataclass +class JewishCalendarData: + """Jewish Calendar runtime dataclass.""" + + language: Language + diaspora: bool + location: Location + candle_lighting_offset: int + havdalah_offset: int + results: JewishCalendarDataResults | None = None + + +class JewishCalendarEntity(Entity): """An HA implementation for Jewish Calendar entity.""" _attr_has_entity_name = True @@ -29,13 +55,23 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]): description: EntityDescription, ) -> None: """Initialize a Jewish Calendar entity.""" - super().__init__(config_entry.runtime_data) self.entity_description = description self._attr_unique_id = f"{config_entry.entry_id}-{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, config_entry.entry_id)}, ) + self.data = config_entry.runtime_data + set_language(self.data.language) + + def make_zmanim(self, date: dt.date) -> Zmanim: + """Create a Zmanim object.""" + return Zmanim( + date=date, + location=self.data.location, + candle_lighting_offset=self.data.candle_lighting_offset, + havdalah_offset=self.data.havdalah_offset, + ) async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" @@ -56,9 +92,10 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]): def _schedule_update(self) -> None: """Schedule the next update of the sensor.""" now = dt_util.now() + zmanim = self.make_zmanim(now.date()) update = dt_util.start_of_local_day() + dt.timedelta(days=1) - for update_time in self._update_times(self.coordinator.zmanim): + for update_time in self._update_times(zmanim): if update_time is not None and now < update_time < update: update = update_time @@ -73,4 +110,17 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]): """Update the sensor data.""" self._update_unsub = None self._schedule_update() + self.create_results(now) self.async_write_ha_state() + + def create_results(self, now: dt.datetime | None = None) -> None: + """Create the results for the sensor.""" + if now is None: + now = dt_util.now() + + _LOGGER.debug("Now: %s Location: %r", now, self.data.location) + + today = now.date() + zmanim = self.make_zmanim(today) + dateinfo = HDateInfo(today, diaspora=self.data.diaspora) + self.data.results = JewishCalendarDataResults(dateinfo, zmanim) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 579c8e0f6a68..d9ad89237f53 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -19,7 +19,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .entity import JewishCalendarConfigEntry, JewishCalendarEntity @@ -236,18 +236,25 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): return [] return [self.entity_description.next_update_fn(zmanim)] - def get_dateinfo(self) -> HDateInfo: + def get_dateinfo(self, now: dt.datetime | None = None) -> HDateInfo: """Get the next date info.""" - now = dt_util.now() + if self.data.results is None: + self.create_results() + assert self.data.results is not None, "Results should be available" + + if now is None: + now = dt_util.now() + + today = now.date() + zmanim = self.make_zmanim(today) update = None - if self.entity_description.next_update_fn: - update = self.entity_description.next_update_fn(self.coordinator.zmanim) + update = self.entity_description.next_update_fn(zmanim) - _LOGGER.debug("Today: %s, update: %s", now.date(), update) + _LOGGER.debug("Today: %s, update: %s", today, update) if update is not None and now >= update: - return self.coordinator.dateinfo.next_day - return self.coordinator.dateinfo + return self.data.results.dateinfo.next_day + return self.data.results.dateinfo class JewishCalendarSensor(JewishCalendarBaseSensor): @@ -264,9 +271,7 @@ class JewishCalendarSensor(JewishCalendarBaseSensor): super().__init__(config_entry, description) # Set the options for enumeration sensors if self.entity_description.options_fn is not None: - self._attr_options = self.entity_description.options_fn( - self.coordinator.data.diaspora - ) + self._attr_options = self.entity_description.options_fn(self.data.diaspora) @property def native_value(self) -> str | int | dt.datetime | None: @@ -290,8 +295,9 @@ class JewishCalendarTimeSensor(JewishCalendarBaseSensor): @property def native_value(self) -> dt.datetime | None: """Return the state of the sensor.""" + if self.data.results is None: + self.create_results() + assert self.data.results is not None, "Results should be available" if self.entity_description.value_fn is None: - return self.coordinator.zmanim.zmanim[self.entity_description.key].local - return self.entity_description.value_fn( - self.get_dateinfo(), self.coordinator.make_zmanim - ) + return self.data.results.zmanim.zmanim[self.entity_description.key].local + return self.entity_description.value_fn(self.get_dateinfo(), self.make_zmanim) diff --git a/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr b/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr index 859cdefd9c2a..0a392e101c51 100644 --- a/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr +++ b/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr @@ -3,15 +3,6 @@ dict({ 'data': dict({ 'candle_lighting_offset': 40, - 'dateinfo': dict({ - 'date': dict({ - 'day': 21, - 'month': 10, - 'year': 5785, - }), - 'diaspora': False, - 'nusach': 'sephardi', - }), 'diaspora': False, 'havdalah_offset': 0, 'language': 'en', @@ -26,22 +17,33 @@ 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", }), }), - 'zmanim': dict({ - 'candle_lighting_offset': 40, - 'date': dict({ - '__type': "", - 'isoformat': '2025-05-19', - }), - 'havdalah_offset': 0, - 'location': dict({ - 'altitude': '**REDACTED**', + 'results': dict({ + 'dateinfo': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), 'diaspora': False, - 'latitude': '**REDACTED**', - 'longitude': '**REDACTED**', - 'name': 'test home', - 'timezone': dict({ - '__type': "", - 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", + 'nusach': 'sephardi', + }), + 'zmanim': dict({ + 'candle_lighting_offset': 40, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', + }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': False, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", + }), }), }), }), @@ -57,15 +59,6 @@ dict({ 'data': dict({ 'candle_lighting_offset': 18, - 'dateinfo': dict({ - 'date': dict({ - 'day': 21, - 'month': 10, - 'year': 5785, - }), - 'diaspora': True, - 'nusach': 'sephardi', - }), 'diaspora': True, 'havdalah_offset': 0, 'language': 'en', @@ -80,22 +73,33 @@ 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", }), }), - 'zmanim': dict({ - 'candle_lighting_offset': 18, - 'date': dict({ - '__type': "", - 'isoformat': '2025-05-19', - }), - 'havdalah_offset': 0, - 'location': dict({ - 'altitude': '**REDACTED**', + 'results': dict({ + 'dateinfo': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), 'diaspora': True, - 'latitude': '**REDACTED**', - 'longitude': '**REDACTED**', - 'name': 'test home', - 'timezone': dict({ - '__type': "", - 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", + 'nusach': 'sephardi', + }), + 'zmanim': dict({ + 'candle_lighting_offset': 18, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', + }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': True, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", + }), }), }), }), @@ -111,15 +115,6 @@ dict({ 'data': dict({ 'candle_lighting_offset': 18, - 'dateinfo': dict({ - 'date': dict({ - 'day': 21, - 'month': 10, - 'year': 5785, - }), - 'diaspora': False, - 'nusach': 'sephardi', - }), 'diaspora': False, 'havdalah_offset': 0, 'language': 'en', @@ -134,22 +129,33 @@ 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", }), }), - 'zmanim': dict({ - 'candle_lighting_offset': 18, - 'date': dict({ - '__type': "", - 'isoformat': '2025-05-19', - }), - 'havdalah_offset': 0, - 'location': dict({ - 'altitude': '**REDACTED**', + 'results': dict({ + 'dateinfo': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), 'diaspora': False, - 'latitude': '**REDACTED**', - 'longitude': '**REDACTED**', - 'name': 'test home', - 'timezone': dict({ - '__type': "", - 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", + 'nusach': 'sephardi', + }), + 'zmanim': dict({ + 'candle_lighting_offset': 18, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', + }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': False, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", + }), }), }), }), From 9dafc0e02f4925e468b57168dea5990707e55a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Wed, 10 Sep 2025 15:34:32 +0200 Subject: [PATCH 25/85] Remove device class for Matter NitrogenDioxideSensor (#151782) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/matter/sensor.py | 2 +- homeassistant/components/matter/strings.json | 3 +++ tests/components/matter/snapshots/test_sensor.ambr | 10 ++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index d8e55b7b1ff9..303a8e94df41 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -634,8 +634,8 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="NitrogenDioxideSensor", + translation_key="nitrogen_dioxide", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.NITROGEN_DIOXIDE, state_class=SensorStateClass.MEASUREMENT, ), entity_class=MatterSensor, diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 9a0bb77adfa7..f36087a0f998 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -435,6 +435,9 @@ "evse_soc": { "name": "State of charge" }, + "nitrogen_dioxide": { + "name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]" + }, "pump_control_mode": { "name": "Control mode", "state": { diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 290016f0ff3e..143ee0a32451 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -353,14 +353,14 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Nitrogen dioxide', 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'nitrogen_dioxide', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-NitrogenDioxideSensor-1043-0', 'unit_of_measurement': 'ppm', }) @@ -368,7 +368,6 @@ # name: test_sensors[air_purifier][sensor.air_purifier_nitrogen_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'nitrogen_dioxide', 'friendly_name': 'Air Purifier Nitrogen dioxide', 'state_class': , 'unit_of_measurement': 'ppm', @@ -955,14 +954,14 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Nitrogen dioxide', 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'nitrogen_dioxide', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-NitrogenDioxideSensor-1043-0', 'unit_of_measurement': 'ppm', }) @@ -970,7 +969,6 @@ # name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_nitrogen_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'nitrogen_dioxide', 'friendly_name': 'lightfi-aq1-air-quality-sensor Nitrogen dioxide', 'state_class': , 'unit_of_measurement': 'ppm', From 1b27acdde00764f96878a95bab7b71cd6e77cc20 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 9 Sep 2025 21:08:04 +0200 Subject: [PATCH 26/85] Improve config entry migration for edge cases in Alexa Devices (#151788) --- .../components/alexa_devices/__init__.py | 24 +++++- .../components/alexa_devices/config_flow.py | 2 +- .../components/alexa_devices/const.py | 1 + tests/components/alexa_devices/conftest.py | 13 +++- .../snapshots/test_diagnostics.ambr | 3 +- tests/components/alexa_devices/test_init.py | 74 +++++++++++++++++-- 6 files changed, 102 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index 9407a2d8987f..af0a3d7818cc 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import _LOGGER, CONF_LOGIN_DATA, COUNTRY_DOMAINS, DOMAIN +from .const import _LOGGER, CONF_LOGIN_DATA, CONF_SITE, COUNTRY_DOMAINS, DOMAIN from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator from .services import async_setup_services @@ -42,7 +42,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Migrate old entry.""" - if entry.version == 1 and entry.minor_version == 1: + + if entry.version == 1 and entry.minor_version < 3: + if CONF_SITE in entry.data: + # Site in data (wrong place), just move to login data + new_data = entry.data.copy() + new_data[CONF_LOGIN_DATA][CONF_SITE] = new_data[CONF_SITE] + new_data.pop(CONF_SITE) + hass.config_entries.async_update_entry( + entry, data=new_data, version=1, minor_version=3 + ) + return True + + if CONF_SITE in entry.data[CONF_LOGIN_DATA]: + # Site is there, just update version to avoid future migrations + hass.config_entries.async_update_entry(entry, version=1, minor_version=3) + return True + _LOGGER.debug( "Migrating from version %s.%s", entry.version, entry.minor_version ) @@ -53,10 +69,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> # Add site to login data new_data = entry.data.copy() - new_data[CONF_LOGIN_DATA]["site"] = f"https://www.amazon.{domain}" + new_data[CONF_LOGIN_DATA][CONF_SITE] = f"https://www.amazon.{domain}" hass.config_entries.async_update_entry( - entry, data=new_data, version=1, minor_version=2 + entry, data=new_data, version=1, minor_version=3 ) _LOGGER.info( diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index ccf18fd45585..f266a8688547 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -52,7 +52,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Alexa Devices.""" VERSION = 1 - MINOR_VERSION = 2 + MINOR_VERSION = 3 async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/alexa_devices/const.py b/homeassistant/components/alexa_devices/const.py index c60096bae571..e783f67f5034 100644 --- a/homeassistant/components/alexa_devices/const.py +++ b/homeassistant/components/alexa_devices/const.py @@ -6,6 +6,7 @@ _LOGGER = logging.getLogger(__package__) DOMAIN = "alexa_devices" CONF_LOGIN_DATA = "login_data" +CONF_SITE = "site" DEFAULT_DOMAIN = "com" COUNTRY_DOMAINS = { diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index 236f7b23dc48..2ef2c2431dc1 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -7,7 +7,11 @@ from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor from aioamazondevices.const import DEVICE_TYPE_TO_MODEL import pytest -from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.components.alexa_devices.const import ( + CONF_LOGIN_DATA, + CONF_SITE, + DOMAIN, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME @@ -81,9 +85,12 @@ def mock_config_entry() -> MockConfigEntry: data={ CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, - CONF_LOGIN_DATA: {"session": "test-session"}, + CONF_LOGIN_DATA: { + "session": "test-session", + CONF_SITE: "https://www.amazon.com", + }, }, unique_id=TEST_USERNAME, version=1, - minor_version=2, + minor_version=3, ) diff --git a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr index 6f9dc9a5cc3e..9ae5832ce334 100644 --- a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr +++ b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr @@ -49,6 +49,7 @@ 'data': dict({ 'login_data': dict({ 'session': 'test-session', + 'site': 'https://www.amazon.com', }), 'password': '**REDACTED**', 'username': '**REDACTED**', @@ -57,7 +58,7 @@ 'discovery_keys': dict({ }), 'domain': 'alexa_devices', - 'minor_version': 2, + 'minor_version': 3, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/alexa_devices/test_init.py b/tests/components/alexa_devices/test_init.py index 6c3faffd27b8..328654682e91 100644 --- a/tests/components/alexa_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -2,9 +2,14 @@ from unittest.mock import AsyncMock +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.components.alexa_devices.const import ( + CONF_LOGIN_DATA, + CONF_SITE, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -32,24 +37,81 @@ async def test_device_info( assert device_entry == snapshot +@pytest.mark.parametrize( + ("minor_version", "extra_data"), + [ + # Standard migration case + ( + 1, + { + CONF_COUNTRY: "US", + CONF_LOGIN_DATA: { + "session": "test-session", + }, + }, + ), + # Edge case #1: no country, site already in login data, minor version 1 + ( + 1, + { + CONF_LOGIN_DATA: { + "session": "test-session", + CONF_SITE: "https://www.amazon.com", + }, + }, + ), + # Edge case #2: no country, site in data (wrong place), minor version 1 + ( + 1, + { + CONF_SITE: "https://www.amazon.com", + CONF_LOGIN_DATA: { + "session": "test-session", + }, + }, + ), + # Edge case #3: no country, site already in login data, minor version 2 + ( + 2, + { + CONF_LOGIN_DATA: { + "session": "test-session", + CONF_SITE: "https://www.amazon.com", + }, + }, + ), + # Edge case #4: no country, site in data (wrong place), minor version 2 + ( + 2, + { + CONF_SITE: "https://www.amazon.com", + CONF_LOGIN_DATA: { + "session": "test-session", + }, + }, + ), + ], +) async def test_migrate_entry( hass: HomeAssistant, mock_amazon_devices_client: AsyncMock, mock_config_entry: MockConfigEntry, + minor_version: int, + extra_data: dict[str, str], ) -> None: """Test successful migration of entry data.""" + config_entry = MockConfigEntry( domain=DOMAIN, title="Amazon Test Account", data={ - CONF_COUNTRY: "US", # country should be in COUNTRY_DOMAINS exceptions CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, - CONF_LOGIN_DATA: {"session": "test-session"}, + **(extra_data), }, unique_id=TEST_USERNAME, version=1, - minor_version=1, + minor_version=minor_version, ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) @@ -57,5 +119,5 @@ async def test_migrate_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.minor_version == 2 - assert config_entry.data[CONF_LOGIN_DATA]["site"] == "https://www.amazon.com" + assert config_entry.minor_version == 3 + assert config_entry.data[CONF_LOGIN_DATA][CONF_SITE] == "https://www.amazon.com" From 0c093646c9032765f79fca5d143d03759cceb5c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Sep 2025 05:57:44 -0500 Subject: [PATCH 27/85] Bump habluetooth to 5.3.1 (#151803) --- 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 5559e5e87103..b4d188550d36 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==5.3.0" + "habluetooth==5.3.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b43da76a609f..c77c07c1551e 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==5.3.0 +habluetooth==5.3.1 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9beabcb8d8c4..e8af66824005 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,7 +1134,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.3.0 +habluetooth==5.3.1 # homeassistant.components.cloud hass-nabucasa==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b3a6caa9752..cb0f84fa76fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -995,7 +995,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.3.0 +habluetooth==5.3.1 # homeassistant.components.cloud hass-nabucasa==1.1.0 From c78bc26b83e0e8a9d40c286e897804a585f8c01c Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 6 Sep 2025 13:02:35 +0200 Subject: [PATCH 28/85] Fix KNX BinarySensor config_store data (#151808) --- .../components/knx/storage/config_store.py | 13 ++++++--- .../components/knx/storage/migration.py | 10 +++++++ .../fixtures/config_store_binarysensor.json | 3 +-- .../config_store_binarysensor_v2_1.json | 27 +++++++++++++++++++ .../knx/fixtures/config_store_light.json | 2 +- tests/components/knx/test_config_store.py | 16 +++++++++++ 6 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 tests/components/knx/fixtures/config_store_binarysensor_v2_1.json diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 2e93256de47d..55505fa64e5e 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -13,11 +13,12 @@ from homeassistant.util.ulid import ulid_now from ..const import DOMAIN from .const import CONF_DATA -from .migration import migrate_1_to_2 +from .migration import migrate_1_to_2, migrate_2_1_to_2_2 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION: Final = 2 +STORAGE_VERSION_MINOR: Final = 2 STORAGE_KEY: Final = f"{DOMAIN}/config_store.json" type KNXPlatformStoreModel = dict[str, dict[str, Any]] # unique_id: configuration @@ -54,9 +55,13 @@ class _KNXConfigStoreStorage(Store[KNXConfigStoreModel]): ) -> dict[str, Any]: """Migrate to the new version.""" if old_major_version == 1: - # version 2 introduced in 2025.8 + # version 2.1 introduced in 2025.8 migrate_1_to_2(old_data) + if old_major_version <= 2 and old_minor_version < 2: + # version 2.2 introduced in 2025.9.2 + migrate_2_1_to_2_2(old_data) + return old_data @@ -71,7 +76,9 @@ class KNXConfigStore: """Initialize config store.""" self.hass = hass self.config_entry = config_entry - self._store = _KNXConfigStoreStorage(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = _KNXConfigStoreStorage( + hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR + ) self.data = KNXConfigStoreModel(entities={}) self._platform_controllers: dict[Platform, PlatformControllerBase] = {} diff --git a/homeassistant/components/knx/storage/migration.py b/homeassistant/components/knx/storage/migration.py index f7d7941e5cc1..fbce1cc7618f 100644 --- a/homeassistant/components/knx/storage/migration.py +++ b/homeassistant/components/knx/storage/migration.py @@ -4,6 +4,7 @@ from typing import Any from homeassistant.const import Platform +from ..const import CONF_RESPOND_TO_READ from . import const as store_const @@ -40,3 +41,12 @@ def _migrate_light_schema_1_to_2(light_knx_data: dict[str, Any]) -> None: if color: light_knx_data[store_const.CONF_COLOR] = color + + +def migrate_2_1_to_2_2(data: dict[str, Any]) -> None: + """Migrate from schema 2.1 to schema 2.2.""" + if b_sensors := data.get("entities", {}).get(Platform.BINARY_SENSOR): + for b_sensor in b_sensors.values(): + # "respond_to_read" was never used for binary_sensor and is not valid + # in the new schema. It was set as default in Store schema v1 and v2.1 + b_sensor["knx"].pop(CONF_RESPOND_TO_READ, None) diff --git a/tests/components/knx/fixtures/config_store_binarysensor.json b/tests/components/knx/fixtures/config_store_binarysensor.json index 2b6e5887f9e9..010149df07dd 100644 --- a/tests/components/knx/fixtures/config_store_binarysensor.json +++ b/tests/components/knx/fixtures/config_store_binarysensor.json @@ -1,6 +1,6 @@ { "version": 2, - "minor_version": 1, + "minor_version": 2, "key": "knx/config_store.json", "data": { "entities": { @@ -17,7 +17,6 @@ "state": "3/2/21", "passive": [] }, - "respond_to_read": false, "sync_state": true } } diff --git a/tests/components/knx/fixtures/config_store_binarysensor_v2_1.json b/tests/components/knx/fixtures/config_store_binarysensor_v2_1.json new file mode 100644 index 000000000000..2b6e5887f9e9 --- /dev/null +++ b/tests/components/knx/fixtures/config_store_binarysensor_v2_1.json @@ -0,0 +1,27 @@ +{ + "version": 2, + "minor_version": 1, + "key": "knx/config_store.json", + "data": { + "entities": { + "light": {}, + "binary_sensor": { + "knx_es_01JJP1XDQRXB0W6YYGXW6Y1X10": { + "entity": { + "name": "test", + "device_info": null, + "entity_category": null + }, + "knx": { + "ga_sensor": { + "state": "3/2/21", + "passive": [] + }, + "respond_to_read": false, + "sync_state": true + } + } + } + } + } +} diff --git a/tests/components/knx/fixtures/config_store_light.json b/tests/components/knx/fixtures/config_store_light.json index 61ec1044746e..e0e1089ed2d6 100644 --- a/tests/components/knx/fixtures/config_store_light.json +++ b/tests/components/knx/fixtures/config_store_light.json @@ -1,6 +1,6 @@ { "version": 2, - "minor_version": 1, + "minor_version": 2, "key": "knx/config_store.json", "data": { "entities": { diff --git a/tests/components/knx/test_config_store.py b/tests/components/knx/test_config_store.py index 3e902f8f4027..bb6af6408b8c 100644 --- a/tests/components/knx/test_config_store.py +++ b/tests/components/knx/test_config_store.py @@ -458,3 +458,19 @@ async def test_migration_1_to_2( hass, "config_store_light.json", "knx" ) assert hass_storage[KNX_CONFIG_STORAGE_KEY] == new_data + + +async def test_migration_2_1_to_2_2( + hass: HomeAssistant, + knx: KNXTestKit, + hass_storage: dict[str, Any], +) -> None: + """Test migration from schema 2.1 to schema 2.2.""" + await knx.setup_integration( + config_store_fixture="config_store_binarysensor_v2_1.json", + state_updater=False, + ) + new_data = await async_load_json_object_fixture( + hass, "config_store_binarysensor.json", "knx" + ) + assert hass_storage[KNX_CONFIG_STORAGE_KEY] == new_data From 64ec4609c5f09b43ca1531d63a411f9161fab8b6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 12 Sep 2025 10:43:45 +0200 Subject: [PATCH 29/85] Fix KNX Light - individual color initialisation from UI config (#151815) --- homeassistant/components/knx/light.py | 14 +++-- .../knx/storage/entity_store_schema.py | 10 ++-- .../knx/snapshots/test_websocket.ambr | 58 +++++++++---------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 1ab6883a4371..bd54e5f75d98 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -285,13 +285,19 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight group_address_switch_green_state=conf.get_state_and_passive( CONF_COLOR, CONF_GA_GREEN_SWITCH ), - group_address_brightness_green=conf.get_write(CONF_GA_GREEN_BRIGHTNESS), + group_address_brightness_green=conf.get_write( + CONF_COLOR, CONF_GA_GREEN_BRIGHTNESS + ), group_address_brightness_green_state=conf.get_state_and_passive( CONF_COLOR, CONF_GA_GREEN_BRIGHTNESS ), - group_address_switch_blue=conf.get_write(CONF_GA_BLUE_SWITCH), - group_address_switch_blue_state=conf.get_state_and_passive(CONF_GA_BLUE_SWITCH), - group_address_brightness_blue=conf.get_write(CONF_GA_BLUE_BRIGHTNESS), + group_address_switch_blue=conf.get_write(CONF_COLOR, CONF_GA_BLUE_SWITCH), + group_address_switch_blue_state=conf.get_state_and_passive( + CONF_COLOR, CONF_GA_BLUE_SWITCH + ), + group_address_brightness_blue=conf.get_write( + CONF_COLOR, CONF_GA_BLUE_BRIGHTNESS + ), group_address_brightness_blue_state=conf.get_state_and_passive( CONF_COLOR, CONF_GA_BLUE_BRIGHTNESS ), diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py index fe0dbf31b6bb..21252e35f3a7 100644 --- a/homeassistant/components/knx/storage/entity_store_schema.py +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -240,19 +240,19 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst( write_required=True, valid_dpt="5.001" ), "section_blue": KNXSectionFlat(), - vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector( - write_required=True, valid_dpt="5.001" - ), vol.Optional(CONF_GA_BLUE_SWITCH): GASelector( write_required=False, valid_dpt="1" ), - "section_white": KNXSectionFlat(), - vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector( + vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector( write_required=True, valid_dpt="5.001" ), + "section_white": KNXSectionFlat(), vol.Optional(CONF_GA_WHITE_SWITCH): GASelector( write_required=False, valid_dpt="1" ), + vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector( + write_required=True, valid_dpt="5.001" + ), }, ), GroupSelectOption( diff --git a/tests/components/knx/snapshots/test_websocket.ambr b/tests/components/knx/snapshots/test_websocket.ambr index b99196c8769b..6dc651195aeb 100644 --- a/tests/components/knx/snapshots/test_websocket.ambr +++ b/tests/components/knx/snapshots/test_websocket.ambr @@ -574,26 +574,6 @@ 'required': False, 'type': 'knx_section_flat', }), - dict({ - 'name': 'ga_blue_brightness', - 'options': dict({ - 'passive': True, - 'state': dict({ - 'required': False, - }), - 'validDPTs': list([ - dict({ - 'main': 5, - 'sub': 1, - }), - ]), - 'write': dict({ - 'required': True, - }), - }), - 'required': True, - 'type': 'knx_group_address', - }), dict({ 'name': 'ga_blue_switch', 'optional': True, @@ -616,14 +596,7 @@ 'type': 'knx_group_address', }), dict({ - 'collapsible': False, - 'name': 'section_white', - 'required': False, - 'type': 'knx_section_flat', - }), - dict({ - 'name': 'ga_white_brightness', - 'optional': True, + 'name': 'ga_blue_brightness', 'options': dict({ 'passive': True, 'state': dict({ @@ -639,9 +612,15 @@ 'required': True, }), }), - 'required': False, + 'required': True, 'type': 'knx_group_address', }), + dict({ + 'collapsible': False, + 'name': 'section_white', + 'required': False, + 'type': 'knx_section_flat', + }), dict({ 'name': 'ga_white_switch', 'optional': True, @@ -663,6 +642,27 @@ 'required': False, 'type': 'knx_group_address', }), + dict({ + 'name': 'ga_white_brightness', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), ]), 'translation_key': 'individual_addresses', 'type': 'knx_group_select_option', From 3af86167648b24f76ffddcf8fc7b3bfc30d95ed4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 7 Sep 2025 13:34:31 +0200 Subject: [PATCH 30/85] Mark Tractive switches as unavailable when tacker is in the enegy saving zone (#151817) --- homeassistant/components/tractive/__init__.py | 1 + homeassistant/components/tractive/switch.py | 3 +- tests/components/tractive/test_switch.py | 40 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 60bae9bfd2e3..f00e0fec4124 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -291,6 +291,7 @@ class TractiveClient: for switch, key in SWITCH_KEY_MAP.items(): if switch_data := event.get(key): payload[switch] = switch_data["active"] + payload[ATTR_POWER_SAVING] = event.get("tracker_state_reason") == "POWER_SAVING" self._dispatch_tracker_event( TRACKER_SWITCH_STATUS_UPDATED, event["tracker_id"], payload ) diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index da2c8e35ff7c..e4db6d69bee1 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -18,6 +18,7 @@ from .const import ( ATTR_BUZZER, ATTR_LED, ATTR_LIVE_TRACKING, + ATTR_POWER_SAVING, TRACKER_SWITCH_STATUS_UPDATED, ) from .entity import TractiveEntity @@ -104,7 +105,7 @@ class TractiveSwitch(TractiveEntity, SwitchEntity): # We received an event, so the service is online and the switch entities should # be available. - self._attr_available = True + self._attr_available = not event[ATTR_POWER_SAVING] self._attr_is_on = event[self.entity_description.key] self.async_write_ha_state() diff --git a/tests/components/tractive/test_switch.py b/tests/components/tractive/test_switch.py index 92e4676aef11..0b9213bee92b 100644 --- a/tests/components/tractive/test_switch.py +++ b/tests/components/tractive/test_switch.py @@ -12,6 +12,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant @@ -226,3 +227,42 @@ async def test_switch_off_with_exception( state = hass.states.get(entity_id) assert state assert state.state == STATE_ON + + +async def test_switch_unavailable( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the switch is navailable when the tracker is in the energy saving zone.""" + entity_id = "switch.test_pet_tracker_buzzer" + + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_switch_event(mock_config_entry) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + event = { + "tracker_id": "device_id_123", + "buzzer_control": {"active": True}, + "led_control": {"active": False}, + "live_tracking": {"active": True}, + "tracker_state_reason": "POWER_SAVING", + } + mock_tractive_client.send_switch_event(mock_config_entry, event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + + mock_tractive_client.send_switch_event(mock_config_entry) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON From c3c65af450049f4d9bcebc0ff9e077bf3d81eda7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 6 Sep 2025 20:21:12 +0200 Subject: [PATCH 31/85] Allow delay > 1 in modbus. (#151832) --- homeassistant/components/modbus/entity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 8667bc17a796..37afb437172a 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -345,7 +345,6 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): return if self._verify_delay: - assert self._verify_delay == 1 if self._cancel_call: self._cancel_call() self._cancel_call = None From b8d9883e74e331a8d02a288539f27251f391deba Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 6 Sep 2025 20:20:49 +0200 Subject: [PATCH 32/85] max_temp / min_temp in modbus light could only be int, otherwise an assert was provoked. (#151833) --- homeassistant/components/modbus/__init__.py | 4 ++-- homeassistant/components/modbus/light.py | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index ab387030af8e..e3bebd159045 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -267,8 +267,8 @@ CLIMATE_SCHEMA = vol.All( { vol.Required(CONF_TARGET_TEMP): hvac_fixedsize_reglist_validator, vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean, - vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(float), - vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(int), + vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(int), vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int, diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index 7b1035c702ba..b5098cb6c469 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -64,7 +64,8 @@ class ModbusLight(BaseSwitch, LightEntity): self._attr_color_mode = self._detect_color_mode(config) self._attr_supported_color_modes = {self._attr_color_mode} - # Set min/max kelvin values if the mode is COLOR_TEMP + self._attr_min_color_temp_kelvin: int = LIGHT_DEFAULT_MIN_KELVIN + self._attr_max_color_temp_kelvin: int = LIGHT_DEFAULT_MAX_KELVIN if self._attr_color_mode == ColorMode.COLOR_TEMP: self._attr_min_color_temp_kelvin = config.get( CONF_MIN_TEMP, LIGHT_DEFAULT_MIN_KELVIN @@ -193,9 +194,6 @@ class ModbusLight(BaseSwitch, LightEntity): def _convert_modbus_percent_to_temperature(self, percent: int) -> int: """Convert Modbus scale (0-100) to the color temperature in Kelvin (2000-7000 К).""" - assert isinstance(self._attr_min_color_temp_kelvin, int) and isinstance( - self._attr_max_color_temp_kelvin, int - ) return round( self._attr_min_color_temp_kelvin + ( @@ -216,9 +214,6 @@ class ModbusLight(BaseSwitch, LightEntity): def _convert_color_temp_to_modbus(self, kelvin: int) -> int: """Convert color temperature from Kelvin to the Modbus scale (0-100).""" - assert isinstance(self._attr_min_color_temp_kelvin, int) and isinstance( - self._attr_max_color_temp_kelvin, int - ) return round( LIGHT_MODBUS_SCALE_MIN + (kelvin - self._attr_min_color_temp_kelvin) From 6d8c35cfe9ecc2802a4bbd1b41606d5d3165dd1c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 6 Sep 2025 20:20:21 +0200 Subject: [PATCH 33/85] removed assert fron entity in modbus. (#151834) --- homeassistant/components/modbus/entity.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 37afb437172a..2bd81ac2ef88 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -94,18 +94,10 @@ class BasePlatform(Entity): self._attr_name = entry[CONF_NAME] self._attr_device_class = entry.get(CONF_DEVICE_CLASS) - def get_optional_numeric_config(config_name: str) -> int | float | None: - if (val := entry.get(config_name)) is None: - return None - assert isinstance(val, (float, int)), ( - f"Expected float or int but {config_name} was {type(val)}" - ) - return val - - self._min_value = get_optional_numeric_config(CONF_MIN_VALUE) - self._max_value = get_optional_numeric_config(CONF_MAX_VALUE) + self._min_value = entry.get(CONF_MIN_VALUE) + self._max_value = entry.get(CONF_MAX_VALUE) self._nan_value = entry.get(CONF_NAN_VALUE) - self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS) + self._zero_suppress = entry.get(CONF_ZERO_SUPPRESS) @abstractmethod async def _async_update(self) -> None: From baff541f46f663913488d21db383abac8ebcbbc6 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sat, 6 Sep 2025 16:07:56 -0400 Subject: [PATCH 34/85] Bump pydrawise to 2025.9.0 (#151842) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index a599ffa888e7..703fed8d4158 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2025.7.0"] + "requirements": ["pydrawise==2025.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e8af66824005..2d850b3fc8f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1934,7 +1934,7 @@ pydiscovergy==3.0.2 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2025.7.0 +pydrawise==2025.9.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb0f84fa76fb..2ae44dec92b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1615,7 +1615,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.2 # homeassistant.components.hydrawise -pydrawise==2025.7.0 +pydrawise==2025.9.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==3.0.0 From 3a615908eeab46a8c347de2b1e6bec913264d0df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Sep 2025 22:55:27 -0500 Subject: [PATCH 35/85] Bump aioharmony to 0.5.3 (#151853) --- homeassistant/components/harmony/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index f67eb4db5aab..f74bff314a45 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/harmony", "iot_class": "local_push", "loggers": ["aioharmony", "slixmpp"], - "requirements": ["aioharmony==0.5.2"], + "requirements": ["aioharmony==0.5.3"], "ssdp": [ { "manufacturer": "Logitech", diff --git a/requirements_all.txt b/requirements_all.txt index 2d850b3fc8f8..3f77df64a3eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -262,7 +262,7 @@ aiogithubapi==24.6.0 aioguardian==2022.07.0 # homeassistant.components.harmony -aioharmony==0.5.2 +aioharmony==0.5.3 # homeassistant.components.hassio aiohasupervisor==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ae44dec92b4..e03ca441459b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -247,7 +247,7 @@ aiogithubapi==24.6.0 aioguardian==2022.07.0 # homeassistant.components.harmony -aioharmony==0.5.2 +aioharmony==0.5.3 # homeassistant.components.hassio aiohasupervisor==0.3.2 From 99b047939f9d88b21b3d6997cadb5a48d67c165d Mon Sep 17 00:00:00 2001 From: Martins Sipenko Date: Sun, 7 Sep 2025 10:36:45 +0300 Subject: [PATCH 36/85] Update pysmarty2 to 0.10.3 (#151855) --- homeassistant/components/smarty/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json index c295647b8e56..fb102a8f9e9f 100644 --- a/homeassistant/components/smarty/manifest.json +++ b/homeassistant/components/smarty/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pymodbus", "pysmarty2"], - "requirements": ["pysmarty2==0.10.2"] + "requirements": ["pysmarty2==0.10.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3f77df64a3eb..eea9753bc585 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2354,7 +2354,7 @@ pysmarlaapi==0.9.2 pysmartthings==3.2.9 # homeassistant.components.smarty -pysmarty2==0.10.2 +pysmarty2==0.10.3 # homeassistant.components.smhi pysmhi==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e03ca441459b..c836d2960dbf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1957,7 +1957,7 @@ pysmarlaapi==0.9.2 pysmartthings==3.2.9 # homeassistant.components.smarty -pysmarty2==0.10.2 +pysmarty2==0.10.3 # homeassistant.components.smhi pysmhi==1.0.2 From 9d904c30a77d845ae5b38769c3924a01c83fde1a Mon Sep 17 00:00:00 2001 From: wollew Date: Thu, 11 Sep 2025 19:14:59 +0200 Subject: [PATCH 37/85] fix rain sensor for Velux GPU windows (#151857) --- homeassistant/components/velux/binary_sensor.py | 5 +++-- tests/components/velux/test_binary_sensor.py | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velux/binary_sensor.py b/homeassistant/components/velux/binary_sensor.py index 15d5d2c89add..de89005fa67d 100644 --- a/homeassistant/components/velux/binary_sensor.py +++ b/homeassistant/components/velux/binary_sensor.py @@ -59,5 +59,6 @@ class VeluxRainSensor(VeluxEntity, BinarySensorEntity): LOGGER.error("Error fetching limitation data for cover %s", self.name) return - # Velux windows with rain sensors report an opening limitation of 93 when rain is detected. - self._attr_is_on = limitation.min_value == 93 + # Velux windows with rain sensors report an opening limitation of 93 or 100 (Velux GPU) when rain is detected. + # So far, only 93 and 100 have been observed in practice, documentation on this is non-existent AFAIK. + self._attr_is_on = limitation.min_value in {93, 100} diff --git a/tests/components/velux/test_binary_sensor.py b/tests/components/velux/test_binary_sensor.py index dfe994b6fa2e..7afd1a0ee7c0 100644 --- a/tests/components/velux/test_binary_sensor.py +++ b/tests/components/velux/test_binary_sensor.py @@ -40,7 +40,14 @@ async def test_rain_sensor_state( assert state is not None assert state.state == STATE_OFF - # simulate rain detected + # simulate rain detected (Velux GPU reports 100) + mock_window.get_limitation.return_value.min_value = 100 + await update_polled_entities(hass, freezer) + state = hass.states.get(test_entity_id) + assert state is not None + assert state.state == STATE_ON + + # simulate rain detected (other Velux models report 93) mock_window.get_limitation.return_value.min_value = 93 freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -49,6 +56,13 @@ async def test_rain_sensor_state( assert state is not None assert state.state == STATE_ON + # simulate no rain detected again + mock_window.get_limitation.return_value.min_value = 95 + await update_polled_entities(hass, freezer) + state = hass.states.get(test_entity_id) + assert state is not None + assert state.state == STATE_OFF + @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.usefixtures("mock_module") From f105b45ee2e608f63fcd1ca63da389f392a2f6ae Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sun, 7 Sep 2025 19:52:05 +0200 Subject: [PATCH 38/85] Bump aioecowitt to 2025.9.1 (#151859) --- homeassistant/components/ecowitt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 0d18933f8775..ba3d01ef6af3 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/ecowitt", "iot_class": "local_push", - "requirements": ["aioecowitt==2025.9.0"] + "requirements": ["aioecowitt==2025.9.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index eea9753bc585..f63ea3bf2d65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -238,7 +238,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2025.9.0 +aioecowitt==2025.9.1 # homeassistant.components.co2signal aioelectricitymaps==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c836d2960dbf..7ffa30f3f285 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2025.9.0 +aioecowitt==2025.9.1 # homeassistant.components.co2signal aioelectricitymaps==1.1.1 From def5408db8412e36b85c75df86fd786bf34690aa Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 12 Sep 2025 10:44:22 +0200 Subject: [PATCH 39/85] Use `native_visibility` property instead of `visibility` for OpenWeatherMap weather entity (#151867) --- homeassistant/components/openweathermap/weather.py | 2 +- tests/components/openweathermap/snapshots/test_weather.ambr | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index f182b083b906..56f44fa46fb7 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -181,7 +181,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator] return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_BEARING) @property - def visibility(self) -> float | str | None: + def native_visibility(self) -> float | None: """Return visibility.""" return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_VISIBILITY_DISTANCE) diff --git a/tests/components/openweathermap/snapshots/test_weather.ambr b/tests/components/openweathermap/snapshots/test_weather.ambr index 073715c87ec6..be3db7bc5943 100644 --- a/tests/components/openweathermap/snapshots/test_weather.ambr +++ b/tests/components/openweathermap/snapshots/test_weather.ambr @@ -72,6 +72,7 @@ 'pressure_unit': , 'temperature': 6.8, 'temperature_unit': , + 'visibility': 10.0, 'visibility_unit': , 'wind_bearing': 199, 'wind_gust_speed': 42.52, @@ -136,6 +137,7 @@ 'supported_features': , 'temperature': 6.8, 'temperature_unit': , + 'visibility': 10.0, 'visibility_unit': , 'wind_bearing': 199, 'wind_gust_speed': 42.52, @@ -200,6 +202,7 @@ 'supported_features': , 'temperature': 6.8, 'temperature_unit': , + 'visibility': 10.0, 'visibility_unit': , 'wind_bearing': 199, 'wind_gust_speed': 42.52, From 12b409d8e1aceaff7144719a219fc14c75344ca9 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:42:49 +0200 Subject: [PATCH 40/85] Bump aiontfy to v0.5.5 (#151869) --- homeassistant/components/ntfy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ntfy/manifest.json b/homeassistant/components/ntfy/manifest.json index f041b02b6d67..ba18dcb4f50a 100644 --- a/homeassistant/components/ntfy/manifest.json +++ b/homeassistant/components/ntfy/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["aionfty"], "quality_scale": "bronze", - "requirements": ["aiontfy==0.5.4"] + "requirements": ["aiontfy==0.5.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index f63ea3bf2d65..b7e8c87d0b9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -325,7 +325,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.4 +aiontfy==0.5.5 # homeassistant.components.nut aionut==4.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ffa30f3f285..c24878b80bfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -307,7 +307,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.4 +aiontfy==0.5.5 # homeassistant.components.nut aionut==4.3.4 From e5b78cc481ecaf7c559f123b0fbec7003786acc9 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 8 Sep 2025 20:45:42 +1000 Subject: [PATCH 41/85] Bump aiolifx-themes to 1.0.2 to support newer LIFX devices (#151898) Signed-off-by: Avi Miller --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 3c755779846b..d7f50ca493bc 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -54,6 +54,6 @@ "requirements": [ "aiolifx==1.2.1", "aiolifx-effects==0.3.2", - "aiolifx-themes==0.6.4" + "aiolifx-themes==1.0.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index b7e8c87d0b9e..96c45f8295c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -298,7 +298,7 @@ aiokem==1.0.1 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.6.4 +aiolifx-themes==1.0.2 # homeassistant.components.lifx aiolifx==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c24878b80bfc..2b4d563a63ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -280,7 +280,7 @@ aiokem==1.0.1 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.6.4 +aiolifx-themes==1.0.2 # homeassistant.components.lifx aiolifx==1.2.1 From f07890cf5c4ad1aa356ee5463336ce7ca7189494 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 8 Sep 2025 12:58:26 +0200 Subject: [PATCH 42/85] Bump aiovodafone to 1.2.1 (#151901) --- homeassistant/components/vodafone_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json index 4c33cf1a4a5a..a9ee2f49b4c6 100644 --- a/homeassistant/components/vodafone_station/manifest.json +++ b/homeassistant/components/vodafone_station/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aiovodafone"], "quality_scale": "platinum", - "requirements": ["aiovodafone==0.10.0"] + "requirements": ["aiovodafone==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 96c45f8295c0..83a416dac0bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -426,7 +426,7 @@ aiousbwatcher==1.1.1 aiovlc==0.5.1 # homeassistant.components.vodafone_station -aiovodafone==0.10.0 +aiovodafone==1.2.1 # homeassistant.components.waqi aiowaqi==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b4d563a63ed..284da0163f4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ aiousbwatcher==1.1.1 aiovlc==0.5.1 # homeassistant.components.vodafone_station -aiovodafone==0.10.0 +aiovodafone==1.2.1 # homeassistant.components.waqi aiowaqi==3.1.0 From 087d9d30c046188774bd0d4d14622be2dde7e643 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 10 Sep 2025 21:09:44 +0200 Subject: [PATCH 43/85] Avoid cleanup/recreate of device_trackers not linked to a device for Vodafone Station (#151904) --- .../vodafone_station/coordinator.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/vodafone_station/coordinator.py b/homeassistant/components/vodafone_station/coordinator.py index 35c32ab2af30..5a3330b16c62 100644 --- a/homeassistant/components/vodafone_station/coordinator.py +++ b/homeassistant/components/vodafone_station/coordinator.py @@ -8,11 +8,14 @@ from typing import Any, cast from aiohttp import ClientSession from aiovodafone import VodafoneStationDevice, VodafoneStationSercommApi, exceptions -from homeassistant.components.device_tracker import DEFAULT_CONSIDER_HOME +from homeassistant.components.device_tracker import ( + DEFAULT_CONSIDER_HOME, + DOMAIN as DEVICE_TRACKER_DOMAIN, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -71,16 +74,14 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]): update_interval=timedelta(seconds=SCAN_INTERVAL), config_entry=config_entry, ) - device_reg = dr.async_get(self.hass) - device_list = dr.async_entries_for_config_entry( - device_reg, self.config_entry.entry_id - ) + entity_reg = er.async_get(hass) self.previous_devices = { - connection[1].upper() - for device in device_list - for connection in device.connections - if connection[0] == dr.CONNECTION_NETWORK_MAC + entry.unique_id + for entry in er.async_entries_for_config_entry( + entity_reg, config_entry.entry_id + ) + if entry.domain == DEVICE_TRACKER_DOMAIN } def _calculate_update_time_and_consider_home( From d6299094db53356af7b4110be3e02f211d5bfa21 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:35:33 +0200 Subject: [PATCH 44/85] Fix _is_valid_suggested_unit in sensor platform (#151912) --- homeassistant/components/sensor/__init__.py | 2 +- tests/components/sensor/test_init.py | 8 + .../tuya/snapshots/test_sensor.ambr | 224 ++++++++++++++++++ 3 files changed, 233 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 561717073387..0268bd8b2071 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -365,7 +365,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): unit converter supports both the native and the suggested units of measurement. """ # Make sure we can convert the units - if ( + if self.native_unit_of_measurement != suggested_unit_of_measurement and ( (unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None or self.__native_unit_of_measurement_compat not in unit_converter.VALID_UNITS diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index ce78edfe481f..c31abe62826a 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -32,6 +32,7 @@ from homeassistant.components.sensor.const import STATE_CLASS_UNITS, UNIT_CONVER from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, STATE_UNKNOWN, EntityCategory, @@ -2938,6 +2939,13 @@ async def test_suggested_unit_guard_invalid_unit( UnitOfDataRate.BITS_PER_SECOND, 10000, ), + ( + SensorDeviceClass.CO2, + CONCENTRATION_PARTS_PER_MILLION, + 10, + CONCENTRATION_PARTS_PER_MILLION, + 10, + ), ], ) async def test_suggested_unit_guard_valid_unit( diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 2a3a93b1b3e7..baee5593ee73 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -400,6 +400,62 @@ 'state': '100.0', }) # --- +# name: test_platform_setup_and_discovery[sensor.aqi_carbon_dioxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aqi_carbon_dioxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'ppm', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'carbon_dioxide', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2occo2_value', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aqi_carbon_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'AQI Carbon dioxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.aqi_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '541.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.aqi_formaldehyde-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -505,6 +561,62 @@ 'state': '53.0', }) # --- +# name: test_platform_setup_and_discovery[sensor.aqi_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.aqi_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'μg/m³', + }), + }), + 'original_device_class': , + '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.iks13mcaiyie3rryjb2ocpm25_value', + 'unit_of_measurement': 'μg/m³', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aqi_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'AQI PM2.5', + 'state_class': , + 'unit_of_measurement': 'μg/m³', + }), + 'context': , + 'entity_id': 'sensor.aqi_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.aqi_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -7063,6 +7175,62 @@ 'state': '42.0', }) # --- +# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_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.kalado_air_purifier_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'μg/m³', + }), + }), + 'original_device_class': , + '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.yo2karkjuhzztxsfjkpm25', + 'unit_of_measurement': 'μg/m³', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Kalado Air Purifier PM2.5', + 'state_class': , + 'unit_of_measurement': 'μg/m³', + }), + 'context': , + 'entity_id': 'sensor.kalado_air_purifier_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.keller_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -11232,6 +11400,62 @@ 'state': '97.0', }) # --- +# name: test_platform_setup_and_discovery[sensor.smogo_carbon_monoxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smogo_carbon_monoxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'ppm', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon monoxide', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'carbon_monoxide', + 'unique_id': 'tuya.swhtzki3qrz5ydchjbocco_value', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smogo_carbon_monoxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_monoxide', + 'friendly_name': 'Smogo Carbon monoxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.smogo_carbon_monoxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.smoke_alarm_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 4618b33e93118325ce50ae487736c5590eb60672 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Sep 2025 13:37:50 -0500 Subject: [PATCH 45/85] Bump habluetooth to 5.5.1 (#151921) --- 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 b4d188550d36..f2009cb07dc7 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==5.3.1" + "habluetooth==5.5.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c77c07c1551e..641ca3fbe1ea 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==5.3.1 +habluetooth==5.5.1 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 83a416dac0bd..4fa3c5ed7209 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,7 +1134,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.3.1 +habluetooth==5.5.1 # homeassistant.components.cloud hass-nabucasa==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 284da0163f4f..107f40c3d5f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -995,7 +995,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.3.1 +habluetooth==5.5.1 # homeassistant.components.cloud hass-nabucasa==1.1.0 From d30ad827743b8ac40085721000a509e5fb216d08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Sep 2025 14:32:31 -0500 Subject: [PATCH 46/85] Bump bleak-esphome to 3.3.0 (#151922) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 802ddae36e93..7253cd79910f 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eq3btsmart"], - "requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.2.0"] + "requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.3.0"] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 8dd198d1da1f..4bf734602ac5 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -19,7 +19,7 @@ "requirements": [ "aioesphomeapi==39.0.1", "esphome-dashboard-api==1.3.0", - "bleak-esphome==3.2.0" + "bleak-esphome==3.3.0" ], "zeroconf": ["_esphomelib._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 4fa3c5ed7209..d13f08cabca2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ bizkaibus==0.1.1 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==3.2.0 +bleak-esphome==3.3.0 # homeassistant.components.bluetooth bleak-retry-connector==4.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 107f40c3d5f1..906525d692ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ bimmer-connected[china]==0.17.3 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==3.2.0 +bleak-esphome==3.3.0 # homeassistant.components.bluetooth bleak-retry-connector==4.4.3 From 6b934d94db23614f17ecbaa788f38a8d9e2be0df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Sep 2025 16:45:11 -0500 Subject: [PATCH 47/85] Bump habluetooth to 5.6.0 (#151942) --- 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 f2009cb07dc7..b87e4d5a2f28 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==5.5.1" + "habluetooth==5.6.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 641ca3fbe1ea..cec1d153f6e6 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==5.5.1 +habluetooth==5.6.0 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index d13f08cabca2..2fe29a567448 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,7 +1134,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.5.1 +habluetooth==5.6.0 # homeassistant.components.cloud hass-nabucasa==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 906525d692ea..2ece92735bed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -995,7 +995,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.5.1 +habluetooth==5.6.0 # homeassistant.components.cloud hass-nabucasa==1.1.0 From 8c61788a7df856a456fd5a75dfb4d41e8c1ae840 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:44:31 +0200 Subject: [PATCH 48/85] Fix invalid logger in Tuya (#151957) --- homeassistant/components/tuya/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 96ee50a38c90..04d68c4ec503 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -42,6 +42,6 @@ "documentation": "https://www.home-assistant.io/integrations/tuya", "integration_type": "hub", "iot_class": "cloud_push", - "loggers": ["tuya_iot"], + "loggers": ["tuya_sharing"], "requirements": ["tuya-device-sharing-sdk==0.2.1"] } From a547179f66774629fe267675cd8f8efa8a6f0e51 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:16:19 +0100 Subject: [PATCH 49/85] Fix for squeezebox track content_type (#151963) --- homeassistant/components/squeezebox/browse_media.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index cebd4fcb04f9..82b6f4b98cda 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -116,6 +116,7 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[ MediaType.APPS: MediaType.APP, MediaType.APP: MediaType.TRACK, "favorite": None, + "track": MediaType.TRACK, } From e5b67d513aa6fee2977ad370cd495613e5078cab Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:22:28 +0100 Subject: [PATCH 50/85] Fix playlist media_class_filter in search_media for squeezebox (#151973) --- 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 a857602a5841..a5f5288807f3 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -607,7 +607,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): _media_content_type_list = ( query.media_content_type.lower().replace(", ", ",").split(",") if query.media_content_type - else ["albums", "tracks", "artists", "genres"] + else ["albums", "tracks", "artists", "genres", "playlists"] ) if query.media_content_type and set(_media_content_type_list).difference( From d6ce71fa61983e6f1d9f1629755f1332d2b8d3fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Sep 2025 11:38:45 -0500 Subject: [PATCH 51/85] Bump habluetooth to 5.6.2 (#151985) --- 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 b87e4d5a2f28..ffffc3ec6f3e 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==5.6.0" + "habluetooth==5.6.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cec1d153f6e6..5a4a5fbf5c96 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==5.6.0 +habluetooth==5.6.2 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 2fe29a567448..5c5324054415 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,7 +1134,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.6.0 +habluetooth==5.6.2 # homeassistant.components.cloud hass-nabucasa==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ece92735bed..08113573e85c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -995,7 +995,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.6.0 +habluetooth==5.6.2 # homeassistant.components.cloud hass-nabucasa==1.1.0 From 529219ae69a4f6a16192a9bd8d2b7e7332c80930 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 10 Sep 2025 01:28:49 +0200 Subject: [PATCH 52/85] Bump yt-dlp to 2025.09.05 (#152006) --- 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 477e77022de7..beb22dd0858b 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.08.11"], + "requirements": ["yt-dlp[default]==2025.09.05"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 5c5324054415..de2b7e628cdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3184,7 +3184,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.08.11 +yt-dlp[default]==2025.09.05 # homeassistant.components.zabbix zabbix-utils==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08113573e85c..36f9520973c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2631,7 +2631,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.08.11 +yt-dlp[default]==2025.09.05 # homeassistant.components.zamg zamg==0.3.6 From ab1c2c4f705a0b71181d2c2fcb3cffb954c8ed94 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 10 Sep 2025 12:25:20 +0200 Subject: [PATCH 53/85] Bump `accuweather` to version 4.2.1 (#152029) --- .../components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/accuweather/test_config_flow.py | 18 ------------------ 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 810557519eb2..9f3c8c7932a7 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -7,6 +7,6 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["accuweather"], - "requirements": ["accuweather==4.2.0"], + "requirements": ["accuweather==4.2.1"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index de2b7e628cdb..5c78ed75c8b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -131,7 +131,7 @@ TwitterAPI==2.7.12 WSDiscovery==2.1.2 # homeassistant.components.accuweather -accuweather==4.2.0 +accuweather==4.2.1 # homeassistant.components.adax adax==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36f9520973c9..0fedb5950d41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ Tami4EdgeAPI==3.0 WSDiscovery==2.1.2 # homeassistant.components.accuweather -accuweather==4.2.0 +accuweather==4.2.1 # homeassistant.components.adax adax==0.4.0 diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index abe1be61905d..63ad8bf55130 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -30,24 +30,6 @@ async def test_show_form(hass: HomeAssistant) -> None: assert result["step_id"] == "user" -async def test_api_key_too_short(hass: HomeAssistant) -> None: - """Test that errors are shown when API key is too short.""" - # The API key length check is done by the library without polling the AccuWeather - # server so we don't need to patch the library method. - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_NAME: "abcd", - CONF_API_KEY: "foo", - CONF_LATITUDE: 55.55, - CONF_LONGITUDE: 122.12, - }, - ) - - assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} - - async def test_invalid_api_key( hass: HomeAssistant, mock_accuweather_client: AsyncMock ) -> None: From 2e33222c7128b0cb750e1664976fbb5a2707de86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Sep 2025 19:45:22 -0500 Subject: [PATCH 54/85] Fix HomeKit Controller stale values at startup (#152086) Co-authored-by: TheJulianJES --- .../homekit_controller/connection.py | 54 ++++++++++-- .../homekit_controller/test_connection.py | 87 +++++++++++++++++++ 2 files changed, 134 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 139ceef48adf..ce8dc498d6d5 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -20,7 +20,12 @@ from aiohomekit.exceptions import ( EncryptionError, ) from aiohomekit.model import Accessories, Accessory, Transport -from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes +from aiohomekit.model.characteristics import ( + EVENT_CHARACTERISTICS, + Characteristic, + CharacteristicPermissions, + CharacteristicsTypes, +) from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.thread import async_get_preferred_dataset @@ -179,6 +184,21 @@ class HKDevice: for aid_iid in characteristics: self.pollable_characteristics.discard(aid_iid) + def get_all_pollable_characteristics(self) -> set[tuple[int, int]]: + """Get all characteristics that can be polled. + + This is used during startup to poll all readable characteristics + before entities have registered what they care about. + """ + return { + (accessory.aid, char.iid) + for accessory in self.entity_map.accessories + for service in accessory.services + for char in service.characteristics + if CharacteristicPermissions.paired_read in char.perms + and char.type not in EVENT_CHARACTERISTICS + } + def add_watchable_characteristics( self, characteristics: list[tuple[int, int]] ) -> None: @@ -309,9 +329,13 @@ class HKDevice: await self.async_process_entity_map() if transport != Transport.BLE: - # Do a single poll to make sure the chars are - # up to date so we don't restore old data. - await self.async_update() + # When Home Assistant starts, we restore the accessory map from storage + # which contains characteristic values from when HA was last running. + # These values are stale and may be incorrect (e.g., Ecobee thermostats + # report 100°C when restarting). We need to poll for fresh values before + # creating entities. Use poll_all=True since entities haven't registered + # their characteristics yet. + await self.async_update(poll_all=True) self._async_start_polling() # If everything is up to date, we can create the entities @@ -863,9 +887,25 @@ class HKDevice: """Request an debounced update from the accessory.""" await self._debounced_update.async_call() - async def async_update(self, now: datetime | None = None) -> None: - """Poll state of all entities attached to this bridge/accessory.""" - to_poll = self.pollable_characteristics + async def async_update( + self, now: datetime | None = None, *, poll_all: bool = False + ) -> None: + """Poll state of all entities attached to this bridge/accessory. + + Args: + now: The current time (used by time interval callbacks). + poll_all: If True, poll all readable characteristics instead + of just the registered ones. + This is useful during initial setup before entities have + registered their characteristics. + """ + if poll_all: + # Poll all readable characteristics during initial startup + # excluding device trigger characteristics (buttons, doorbell, etc.) + to_poll = self.get_all_pollable_characteristics() + else: + to_poll = self.pollable_characteristics + if not to_poll: self.async_update_available_state() _LOGGER.debug( diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 00c7bb162599..99203d400fea 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -2,6 +2,7 @@ from collections.abc import Callable import dataclasses +from typing import Any from unittest import mock from aiohomekit.controller import TransportType @@ -11,6 +12,7 @@ from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.testing import FakeController import pytest +from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE from homeassistant.components.homekit_controller.const import ( DEBOUNCE_COOLDOWN, DOMAIN, @@ -439,3 +441,88 @@ async def test_manual_poll_all_chars( await time_changed(hass, DEBOUNCE_COOLDOWN) await hass.async_block_till_done() assert len(mock_get_characteristics.call_args_list[0][0][0]) > 1 + + +async def test_poll_all_on_startup_refreshes_stale_values( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test that entities get fresh values on startup instead of stale stored values.""" + # Load actual Ecobee accessory fixture + accessories = await setup_accessories_from_file(hass, "ecobee3.json") + + # Pre-populate storage with the accessories data (already has stale values) + hass_storage["homekit_controller-entity-map"] = { + "version": 1, + "minor_version": 1, + "key": "homekit_controller-entity-map", + "data": { + "pairings": { + "00:00:00:00:00:00": { + "config_num": 1, + "accessories": [ + a.to_accessory_and_service_list() for a in accessories + ], + } + } + }, + } + + # Track what gets polled during setup + polled_chars: list[tuple[int, int]] = [] + + # Set up the test accessories + fake_controller = await setup_platform(hass) + + # Mock get_characteristics to track polling and return fresh temperature + async def mock_get_characteristics( + chars: set[tuple[int, int]], **kwargs: Any + ) -> dict[tuple[int, int], dict[str, Any]]: + """Return fresh temperature value when polled.""" + polled_chars.extend(chars) + # Return fresh values for all characteristics + result: dict[tuple[int, int], dict[str, Any]] = {} + for aid, iid in chars: + # Find the characteristic and return appropriate value + for accessory in accessories: + if accessory.aid != aid: + continue + for service in accessory.services: + for char in service.characteristics: + if char.iid != iid: + continue + # Return fresh temperature instead of stale fixture value + if char.type == CharacteristicsTypes.TEMPERATURE_CURRENT: + result[(aid, iid)] = {"value": 22.5} # Fresh value + else: + result[(aid, iid)] = {"value": char.value} + break + return result + + # Add the paired device with our mock + await fake_controller.add_paired_device(accessories, "00:00:00:00:00:00") + config_entry = MockConfigEntry( + version=1, + domain="homekit_controller", + entry_id="TestData", + data={"AccessoryPairingID": "00:00:00:00:00:00"}, + title="test", + ) + config_entry.add_to_hass(hass) + + # Get the pairing and patch its get_characteristics + pairing = fake_controller.pairings["00:00:00:00:00:00"] + + with mock.patch.object(pairing, "get_characteristics", mock_get_characteristics): + # Set up the config entry (this should trigger poll_all=True) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that polling happened during setup (poll_all=True was used) + assert ( + len(polled_chars) == 79 + ) # The Ecobee fixture has exactly 79 readable characteristics + + # Check that the climate entity has the fresh temperature (22.5°C) not the stale fixture value (21.8°C) + state = hass.states.get("climate.homew") + assert state is not None + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 From 9c749a6abc399cb0127e80571d5c8831cac080a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 11 Sep 2025 14:47:59 +0100 Subject: [PATCH 55/85] Fix duplicated IP port usage in Govee Light Local (#152087) --- .../components/govee_light_local/__init__.py | 19 ++++++++++------ .../govee_light_local/config_flow.py | 17 +++++--------- .../govee_light_local/coordinator.py | 5 ++--- .../govee_light_local/test_config_flow.py | 22 ++++++++++++------- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py index 803f4b3ead54..4315f5d5363d 100644 --- a/homeassistant/components/govee_light_local/__init__.py +++ b/homeassistant/components/govee_light_local/__init__.py @@ -26,16 +26,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool: """Set up Govee light local from a config entry.""" - # Get source IPs for all enabled adapters - source_ips = await network.async_get_enabled_source_ips(hass) + source_ips = await async_get_source_ips(hass) _LOGGER.debug("Enabled source IPs: %s", source_ips) coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator( - hass=hass, - config_entry=entry, - source_ips=[ - source_ip for source_ip in source_ips if isinstance(source_ip, IPv4Address) - ], + hass=hass, config_entry=entry, source_ips=source_ips ) async def await_cleanup(): @@ -76,3 +71,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) - async def async_unload_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_get_source_ips( + hass: HomeAssistant, +) -> set[str]: + """Get the source ips for Govee local.""" + source_ips = await network.async_get_enabled_source_ips(hass) + return { + str(source_ip) for source_ip in source_ips if isinstance(source_ip, IPv4Address) + } diff --git a/homeassistant/components/govee_light_local/config_flow.py b/homeassistant/components/govee_light_local/config_flow.py index a1f601b28889..cd1dc00f9e0e 100644 --- a/homeassistant/components/govee_light_local/config_flow.py +++ b/homeassistant/components/govee_light_local/config_flow.py @@ -4,15 +4,14 @@ from __future__ import annotations import asyncio from contextlib import suppress -from ipaddress import IPv4Address import logging from govee_local_api import GoveeController -from homeassistant.components import network from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow +from . import async_get_source_ips from .const import ( CONF_LISTENING_PORT_DEFAULT, CONF_MULTICAST_ADDRESS_DEFAULT, @@ -24,11 +23,11 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def _async_discover(hass: HomeAssistant, adapter_ip: IPv4Address) -> bool: +async def _async_discover(hass: HomeAssistant, adapter_ip: str) -> bool: controller: GoveeController = GoveeController( loop=hass.loop, logger=_LOGGER, - listening_address=str(adapter_ip), + listening_address=adapter_ip, broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT, broadcast_port=CONF_TARGET_PORT_DEFAULT, listening_port=CONF_LISTENING_PORT_DEFAULT, @@ -62,14 +61,8 @@ async def _async_discover(hass: HomeAssistant, adapter_ip: IPv4Address) -> bool: async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" - # Get source IPs for all enabled adapters - source_ips = await network.async_get_enabled_source_ips(hass) - _LOGGER.debug("Enabled source IPs: %s", source_ips) - - # Run discovery on every IPv4 address and gather results - results = await asyncio.gather( - *[_async_discover(hass, ip) for ip in source_ips if isinstance(ip, IPv4Address)] - ) + source_ips = await async_get_source_ips(hass) + results = await asyncio.gather(*[_async_discover(hass, ip) for ip in source_ips]) return any(results) diff --git a/homeassistant/components/govee_light_local/coordinator.py b/homeassistant/components/govee_light_local/coordinator.py index 31efeb55680d..1c2aac12f70c 100644 --- a/homeassistant/components/govee_light_local/coordinator.py +++ b/homeassistant/components/govee_light_local/coordinator.py @@ -2,7 +2,6 @@ import asyncio from collections.abc import Callable -from ipaddress import IPv4Address import logging from govee_local_api import GoveeController, GoveeDevice @@ -30,7 +29,7 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]): self, hass: HomeAssistant, config_entry: GoveeLocalConfigEntry, - source_ips: list[IPv4Address], + source_ips: set[str], ) -> None: """Initialize my coordinator.""" super().__init__( @@ -45,7 +44,7 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]): GoveeController( loop=hass.loop, logger=_LOGGER, - listening_address=str(source_ip), + listening_address=source_ip, broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT, broadcast_port=CONF_TARGET_PORT_DEFAULT, listening_port=CONF_LISTENING_PORT_DEFAULT, diff --git a/tests/components/govee_light_local/test_config_flow.py b/tests/components/govee_light_local/test_config_flow.py index e6e336a70f29..32ef2408c013 100644 --- a/tests/components/govee_light_local/test_config_flow.py +++ b/tests/components/govee_light_local/test_config_flow.py @@ -1,6 +1,7 @@ """Test Govee light local config flow.""" from errno import EADDRINUSE +from ipaddress import IPv4Address from unittest.mock import AsyncMock, patch from govee_local_api import GoveeDevice @@ -61,17 +62,22 @@ async def test_creating_entry_has_with_devices( mock_govee_api.devices = _get_devices(mock_govee_api) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + # Mock duplicated IPs to ensure that only one GoveeController is started + with patch( + "homeassistant.components.network.async_get_enabled_source_ips", + return_value=[IPv4Address("192.168.1.2"), IPv4Address("192.168.1.2")], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - # Confirmation form - assert result["type"] is FlowResultType.FORM + # Confirmation form + assert result["type"] is FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.CREATE_ENTRY + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() + await hass.async_block_till_done() mock_govee_api.start.assert_awaited_once() mock_setup_entry.assert_awaited_once() From 9a165a64fe28c662efd121f365b3e60f697abdcd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Sep 2025 09:58:59 -0500 Subject: [PATCH 56/85] Fix DoorBird being updated with wrong IP addresses during discovery (#152088) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/doorbird/config_flow.py | 56 ++++++++++++- .../components/doorbird/strings.json | 2 + tests/components/doorbird/test_config_flow.py | 84 ++++++++++++++++++- 3 files changed, 140 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 6a954f5310ff..ac08ad0e1f62 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -19,8 +19,10 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.helpers.typing import VolDictType @@ -103,6 +105,43 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN): """Initialize the DoorBird config flow.""" self.discovery_schema: vol.Schema | None = None + async def _async_verify_existing_device_for_discovery( + self, + existing_entry: ConfigEntry, + host: str, + macaddress: str, + ) -> None: + """Verify discovered device matches existing entry before updating IP. + + This method performs the following verification steps: + 1. Ensures that the stored credentials work before updating the entry. + 2. Verifies that the device at the discovered IP address has the expected MAC address. + """ + info, errors = await self._async_validate_or_error( + { + **existing_entry.data, + CONF_HOST: host, + } + ) + + if errors: + _LOGGER.debug( + "Cannot validate DoorBird at %s with existing credentials: %s", + host, + errors, + ) + raise AbortFlow("cannot_connect") + + # Verify the MAC address matches what was advertised + if format_mac(info["mac_addr"]) != format_mac(macaddress): + _LOGGER.debug( + "DoorBird at %s reports MAC %s but zeroconf advertised %s, ignoring", + host, + info["mac_addr"], + macaddress, + ) + raise AbortFlow("wrong_device") + async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: @@ -172,7 +211,22 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(macaddress) host = discovery_info.host - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + # Check if we have an existing entry for this MAC + existing_entry = self.hass.config_entries.async_entry_for_domain_unique_id( + DOMAIN, macaddress + ) + + if existing_entry: + # Check if the host is actually changing + if existing_entry.data.get(CONF_HOST) != host: + await self._async_verify_existing_device_for_discovery( + existing_entry, host, macaddress + ) + + # All checks passed or no change needed, abort + # if already configured with potential IP update + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: host}) diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 285b544e465b..341976e8a8f1 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -49,6 +49,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "link_local_address": "Link local addresses are not supported", "not_doorbird_device": "This device is not a DoorBird", + "not_ipv4_address": "Only IPv4 addresses are supported", + "wrong_device": "Device MAC address does not match", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "flow_title": "{name} ({host})", diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 98b2189dfd97..493762df5ef7 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -108,7 +108,9 @@ async def test_form_zeroconf_link_local_ignored(hass: HomeAssistant) -> None: assert result["reason"] == "link_local_address" -async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None: +async def test_form_zeroconf_ipv4_address( + hass: HomeAssistant, doorbird_api: DoorBird +) -> None: """Test we abort and update the ip address from zeroconf with an ipv4 address.""" config_entry = MockConfigEntry( @@ -118,6 +120,13 @@ async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None: options={CONF_EVENTS: ["event1", "event2", "event3"]}, ) config_entry.add_to_hass(hass) + + # Mock the API to return the correct MAC when validating + doorbird_api.info.return_value = { + "PRIMARY_MAC_ADDR": "1CCAE3AAAAAA", + "WIFI_MAC_ADDR": "1CCAE3BBBBBB", + } + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -136,6 +145,79 @@ async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None: assert config_entry.data[CONF_HOST] == "4.4.4.4" +async def test_form_zeroconf_ipv4_address_wrong_device( + hass: HomeAssistant, doorbird_api: DoorBird +) -> None: + """Test we abort when the device MAC doesn't match during zeroconf update.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="1CCAE3AAAAAA", + data=VALID_CONFIG, + options={CONF_EVENTS: ["event1", "event2", "event3"]}, + ) + config_entry.add_to_hass(hass) + + # Mock the API to return a different MAC (wrong device) + doorbird_api.info.return_value = { + "PRIMARY_MAC_ADDR": "1CCAE3DIFFERENT", # Different MAC! + "WIFI_MAC_ADDR": "1CCAE3BBBBBB", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("4.4.4.4"), + ip_addresses=[ip_address("4.4.4.4")], + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3AAAAAA"}, + type="mock_type", + ), + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_device" + # Host should not be updated since it's the wrong device + assert config_entry.data[CONF_HOST] == "1.2.3.4" + + +async def test_form_zeroconf_ipv4_address_cannot_connect( + hass: HomeAssistant, doorbird_api: DoorBird +) -> None: + """Test we abort when we cannot connect to validate during zeroconf update.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="1CCAE3AAAAAA", + data=VALID_CONFIG, + options={CONF_EVENTS: ["event1", "event2", "event3"]}, + ) + config_entry.add_to_hass(hass) + + # Mock the API to fail connection (e.g., wrong credentials or network error) + doorbird_api.info.side_effect = mock_unauthorized_exception() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("4.4.4.4"), + ip_addresses=[ip_address("4.4.4.4")], + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3AAAAAA"}, + type="mock_type", + ), + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + # Host should not be updated since we couldn't validate + assert config_entry.data[CONF_HOST] == "1.2.3.4" + + async def test_form_zeroconf_non_ipv4_ignored(hass: HomeAssistant) -> None: """Test we abort when we get a non ipv4 address via zeroconf.""" From d2e753762913fb762869cca438b2fe0074403160 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 11 Sep 2025 19:43:44 +0200 Subject: [PATCH 57/85] Fix supported _color_modes attribute not set for on/off MQTT JSON light (#152126) --- homeassistant/components/mqtt/light/schema_json.py | 2 ++ tests/components/mqtt/test_light_json.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index fc76d4bcf6c6..f71a333dbe10 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -223,6 +223,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): # Brightness is supported and no supported_color_modes are set, # so set brightness as the supported color mode. self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + else: + self._attr_supported_color_modes = {ColorMode.ONOFF} def _update_color(self, values: dict[str, Any]) -> None: color_mode: str = values["color_mode"] diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 7f7f32c4e43a..8c32926e08e8 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -182,6 +182,19 @@ class JsonValidator: return json_loads(self.jsondata) == json_loads(other) +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_simple_on_off_light( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test if setup fails with no command topic.""" + assert await mqtt_mock_entry() + state = hass.states.get("light.test") + assert state and state.state == STATE_UNKNOWN + assert state.attributes["supported_color_modes"] == ["onoff"] + + @pytest.mark.parametrize( "hass_config", [{mqtt.DOMAIN: {light.DOMAIN: {"schema": "json", "name": "test"}}}] ) From 14173bd9ecf05613870ba7fbab88e3ae197d2a2a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 11 Sep 2025 22:26:47 +0200 Subject: [PATCH 58/85] Fix reauth for Alexa Devices (#152128) --- .../components/alexa_devices/config_flow.py | 7 +++- tests/components/alexa_devices/conftest.py | 1 + .../alexa_devices/test_config_flow.py | 38 +++++++++++++++++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index f266a8688547..a3bcce1965b2 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -107,7 +107,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: - await validate_input(self.hass, {**reauth_entry.data, **user_input}) + data = await validate_input( + self.hass, {**reauth_entry.data, **user_input} + ) except CannotConnect: errors["base"] = "cannot_connect" except (CannotAuthenticate, TypeError): @@ -119,8 +121,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): reauth_entry, data={ CONF_USERNAME: entry_data[CONF_USERNAME], - CONF_PASSWORD: entry_data[CONF_PASSWORD], + CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_CODE: user_input[CONF_CODE], + CONF_LOGIN_DATA: data, }, ) diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index 2ef2c2431dc1..7397b4b72fb5 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -45,6 +45,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: client = mock_client.return_value client.login_mode_interactive.return_value = { "customer_info": {"user_id": TEST_USERNAME}, + CONF_SITE: "https://www.amazon.com", } client.get_devices_data.return_value = { TEST_SERIAL_NUMBER: AmazonDevice( diff --git a/tests/components/alexa_devices/test_config_flow.py b/tests/components/alexa_devices/test_config_flow.py index 9aea6fe4c44f..4722f9c0c5f2 100644 --- a/tests/components/alexa_devices/test_config_flow.py +++ b/tests/components/alexa_devices/test_config_flow.py @@ -9,7 +9,11 @@ from aioamazondevices.exceptions import ( ) import pytest -from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.components.alexa_devices.const import ( + CONF_LOGIN_DATA, + CONF_SITE, + DOMAIN, +) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -48,6 +52,7 @@ async def test_full_flow( CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: { "customer_info": {"user_id": TEST_USERNAME}, + CONF_SITE: "https://www.amazon.com", }, } assert result["result"].unique_id == TEST_USERNAME @@ -158,6 +163,16 @@ async def test_reauth_successful( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" + assert mock_config_entry.data == { + CONF_CODE: "000000", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: "other_fake_password", + CONF_LOGIN_DATA: { + "customer_info": {"user_id": TEST_USERNAME}, + CONF_SITE: "https://www.amazon.com", + }, + } + @pytest.mark.parametrize( ("side_effect", "error"), @@ -206,8 +221,15 @@ async def test_reauth_not_successful( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert mock_config_entry.data[CONF_PASSWORD] == "fake_password" - assert mock_config_entry.data[CONF_CODE] == "111111" + assert mock_config_entry.data == { + CONF_CODE: "111111", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: "fake_password", + CONF_LOGIN_DATA: { + "customer_info": {"user_id": TEST_USERNAME}, + CONF_SITE: "https://www.amazon.com", + }, + } async def test_reconfigure_successful( @@ -240,7 +262,14 @@ async def test_reconfigure_successful( assert reconfigure_result["reason"] == "reconfigure_successful" # changed entry - assert mock_config_entry.data[CONF_PASSWORD] == new_password + assert mock_config_entry.data == { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: new_password, + CONF_LOGIN_DATA: { + "customer_info": {"user_id": TEST_USERNAME}, + CONF_SITE: "https://www.amazon.com", + }, + } @pytest.mark.parametrize( @@ -297,5 +326,6 @@ async def test_reconfigure_fails( CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: { "customer_info": {"user_id": TEST_USERNAME}, + CONF_SITE: "https://www.amazon.com", }, } From dc09e335564c09079a69df273109102e9b873b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 12 Sep 2025 10:18:58 +0200 Subject: [PATCH 59/85] Bump hass-nabucasa from 1.1.0 to 1.1.1 (#152147) --- 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 43cdf17740a8..0625054869d8 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==1.1.0"], + "requirements": ["hass-nabucasa==1.1.1"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5a4a5fbf5c96..f12009ee9396 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==5.6.2 -hass-nabucasa==1.1.0 +hass-nabucasa==1.1.1 hassil==3.2.0 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250903.3 diff --git a/pyproject.toml b/pyproject.toml index ee06c96403bb..abcd1e0d5491 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==1.1.0", + "hass-nabucasa==1.1.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 d1de18296ff5..381d1fd98d8d 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==1.1.0 +hass-nabucasa==1.1.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 5c78ed75c8b9..ceff657f574d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1137,7 +1137,7 @@ habiticalib==0.4.3 habluetooth==5.6.2 # homeassistant.components.cloud -hass-nabucasa==1.1.0 +hass-nabucasa==1.1.1 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fedb5950d41..5384b8661f0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ habiticalib==0.4.3 habluetooth==5.6.2 # homeassistant.components.cloud -hass-nabucasa==1.1.0 +hass-nabucasa==1.1.1 # homeassistant.components.assist_satellite # homeassistant.components.conversation From a764d541239ef2fbf59d8bd57329a190f2484814 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 12 Sep 2025 18:01:16 +0200 Subject: [PATCH 60/85] Update frontend to 20250903.5 (#152170) --- 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 d74bf1f30b7f..44dff4502993 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==20250903.3"] + "requirements": ["home-assistant-frontend==20250903.5"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f12009ee9396..587fff7deb52 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.6.2 hass-nabucasa==1.1.1 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250903.3 +home-assistant-frontend==20250903.5 home-assistant-intents==2025.9.3 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ceff657f574d..10ff4d25da7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.3 +home-assistant-frontend==20250903.5 # homeassistant.components.conversation home-assistant-intents==2025.9.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5384b8661f0d..d288c67d03cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.3 +home-assistant-frontend==20250903.5 # homeassistant.components.conversation home-assistant-intents==2025.9.3 From 91a7db08ffa8b9599dbd1bd1a34a28e6ee7ff4f1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Sep 2025 20:20:56 +0000 Subject: [PATCH 61/85] Bump version to 2025.9.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 318594196e27..17a8523d069d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -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 abcd1e0d5491..cf16f1815829 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.1" +version = "2025.9.2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 1c10b85fed57cb8c13ea586c405d1aef4ae3b5d8 Mon Sep 17 00:00:00 2001 From: wollew Date: Fri, 5 Sep 2025 17:45:29 +0200 Subject: [PATCH 62/85] Use position percentage for closed status in Velux (#151679) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CODEOWNERS | 4 +- homeassistant/components/velux/cover.py | 16 +++++-- homeassistant/components/velux/manifest.json | 2 +- tests/components/velux/__init__.py | 30 ++++++++++++ tests/components/velux/conftest.py | 3 ++ tests/components/velux/test_binary_sensor.py | 13 ++---- tests/components/velux/test_cover.py | 48 ++++++++++++++++++++ 7 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 tests/components/velux/test_cover.py diff --git a/CODEOWNERS b/CODEOWNERS index d1f06d04b41d..d514bb18bafb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1690,8 +1690,8 @@ build.json @home-assistant/supervisor /tests/components/vegehub/ @ghowevege /homeassistant/components/velbus/ @Cereal2nd @brefra /tests/components/velbus/ @Cereal2nd @brefra -/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio -/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio +/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew +/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew /homeassistant/components/venstar/ @garbled1 @jhollowe /tests/components/venstar/ @garbled1 @jhollowe /homeassistant/components/versasense/ @imstevenxyz diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 32be29c3c914..f31c4877ffdd 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -4,8 +4,15 @@ from __future__ import annotations from typing import Any, cast -from pyvlx import OpeningDevice, Position -from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter +from pyvlx import ( + Awning, + Blind, + GarageDoor, + Gate, + OpeningDevice, + Position, + RollerShutter, +) from homeassistant.components.cover import ( ATTR_POSITION, @@ -97,7 +104,10 @@ class VeluxCover(VeluxEntity, CoverEntity): @property def is_closed(self) -> bool: """Return if the cover is closed.""" - return self.node.position.closed + # do not use the node's closed state but rely on cover position + # until https://github.com/Julius2342/pyvlx/pull/543 is merged. + # once merged this can again return self.node.position.closed + return self.current_cover_position == 0 @property def is_opening(self) -> bool: diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index cb21fef299df..11e939fdfe77 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -1,7 +1,7 @@ { "domain": "velux", "name": "Velux", - "codeowners": ["@Julius2342", "@DeerMaximum", "@pawlizio"], + "codeowners": ["@Julius2342", "@DeerMaximum", "@pawlizio", "@wollew"], "config_flow": true, "dhcp": [ { diff --git a/tests/components/velux/__init__.py b/tests/components/velux/__init__.py index 6cf5cd366fb1..b50a46b1150d 100644 --- a/tests/components/velux/__init__.py +++ b/tests/components/velux/__init__.py @@ -1 +1,31 @@ """Tests for the Velux integration.""" + +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.helpers.device_registry import HomeAssistant +from homeassistant.helpers.entity_platform import timedelta + +from tests.common import async_fire_time_changed + + +async def update_callback_entity( + hass: HomeAssistant, mock_velux_node: MagicMock +) -> None: + """Simulate an update triggered by the pyvlx lib for a Velux node.""" + + callback = mock_velux_node.register_device_updated_cb.call_args[0][0] + await callback(mock_velux_node) + await hass.async_block_till_done() + + +async def update_polled_entities( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Simulate an update trigger from polling.""" + # just fire a time changed event to trigger the polling + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() diff --git a/tests/components/velux/conftest.py b/tests/components/velux/conftest.py index 1b7066577ad4..22fc1a933579 100644 --- a/tests/components/velux/conftest.py +++ b/tests/components/velux/conftest.py @@ -72,6 +72,9 @@ def mock_window() -> AsyncMock: window.rain_sensor = True window.serial_number = "123456789" window.get_limitation.return_value = MagicMock(min_value=0) + window.is_opening = False + window.is_closing = False + window.position = MagicMock(position_percent=30, closed=False) return window diff --git a/tests/components/velux/test_binary_sensor.py b/tests/components/velux/test_binary_sensor.py index 7afd1a0ee7c0..b7048173a657 100644 --- a/tests/components/velux/test_binary_sensor.py +++ b/tests/components/velux/test_binary_sensor.py @@ -1,6 +1,5 @@ """Tests for the Velux binary sensor platform.""" -from datetime import timedelta from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory @@ -11,7 +10,9 @@ 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 +from . import update_polled_entities + +from tests.common import MockConfigEntry @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -33,9 +34,7 @@ async def test_rain_sensor_state( test_entity_id = "binary_sensor.test_window_rain_sensor" # simulate no rain detected - freezer.tick(timedelta(minutes=5)) - async_fire_time_changed(hass) - await hass.async_block_till_done() + await update_polled_entities(hass, freezer) state = hass.states.get(test_entity_id) assert state is not None assert state.state == STATE_OFF @@ -49,9 +48,7 @@ async def test_rain_sensor_state( # simulate rain detected (other Velux models report 93) mock_window.get_limitation.return_value.min_value = 93 - freezer.tick(timedelta(minutes=5)) - async_fire_time_changed(hass) - await hass.async_block_till_done() + await update_polled_entities(hass, freezer) state = hass.states.get(test_entity_id) assert state is not None assert state.state == STATE_ON diff --git a/tests/components/velux/test_cover.py b/tests/components/velux/test_cover.py new file mode 100644 index 000000000000..621aa1c3b6c9 --- /dev/null +++ b/tests/components/velux/test_cover.py @@ -0,0 +1,48 @@ +"""Tests for the Velux cover platform.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.const import STATE_CLOSED, STATE_OPEN, Platform +from homeassistant.core import HomeAssistant + +from . import update_callback_entity + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("mock_module") +async def test_cover_closed( + hass: HomeAssistant, + mock_window: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the cover closed state.""" + + mock_config_entry.add_to_hass(hass) + with patch("homeassistant.components.velux.PLATFORMS", [Platform.COVER]): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + test_entity_id = "cover.test_window" + + # Initial state should be open + state = hass.states.get(test_entity_id) + assert state is not None + assert state.state == STATE_OPEN + + # Update mock window position to closed percentage + mock_window.position.position_percent = 100 + # Also directly set position to closed, so this test should + # continue to be green after the lib is fixed + mock_window.position.closed = True + + # Trigger entity state update via registered callback + await update_callback_entity(hass, mock_window) + + state = hass.states.get(test_entity_id) + assert state is not None + assert state.state == STATE_CLOSED From 40988198f36efd96e353322faa899baa52521145 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 06:04:58 -0500 Subject: [PATCH 63/85] Bump habluetooth to 5.6.4 (#152227) --- 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 ffffc3ec6f3e..431ec10b366d 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==5.6.2" + "habluetooth==5.6.4" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 587fff7deb52..7da074c28eaa 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==5.6.2 +habluetooth==5.6.4 hass-nabucasa==1.1.1 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 10ff4d25da7d..8b1d6bd2f33b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,7 +1134,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.6.2 +habluetooth==5.6.4 # homeassistant.components.cloud hass-nabucasa==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d288c67d03cc..67b707eb2612 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -995,7 +995,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.6.2 +habluetooth==5.6.4 # homeassistant.components.cloud hass-nabucasa==1.1.1 From 2c809d59033840f2d0af8a677e830996f59ad6ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 13 Sep 2025 12:14:15 +0000 Subject: [PATCH 64/85] Bump version to 2025.9.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 17a8523d069d..217247dedfcc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -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 cf16f1815829..ef4603faa818 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.2" +version = "2025.9.3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From eac719f9afb4d121b1c05c3190698e780b6f6496 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 29 Aug 2025 00:04:37 +0200 Subject: [PATCH 65/85] Bump habiticalib to v0.4.4 (#151332) --- 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 99c84f9686f5..86002107a687 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.3"] + "requirements": ["habiticalib==0.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b1d6bd2f33b..65494bafc363 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1131,7 +1131,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.3 +habiticalib==0.4.4 # homeassistant.components.bluetooth habluetooth==5.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67b707eb2612..b0ae6b9dd408 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -992,7 +992,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.3 +habiticalib==0.4.4 # homeassistant.components.bluetooth habluetooth==5.6.4 From 8920c548d508ce5199d73460cc295f544ddf74b8 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:58:28 +0200 Subject: [PATCH 66/85] Bump habiticalib to v0.4.5 (#151720) --- 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 86002107a687..30443f1d1da8 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.4"] + "requirements": ["habiticalib==0.4.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 65494bafc363..89d03562f653 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1131,7 +1131,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.4 +habiticalib==0.4.5 # homeassistant.components.bluetooth habluetooth==5.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0ae6b9dd408..89d9b6d11a8f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -992,7 +992,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.4 +habiticalib==0.4.5 # homeassistant.components.bluetooth habluetooth==5.6.4 From b30667a469d1653fa62b5e6a0ef511b1529144c5 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Tue, 16 Sep 2025 21:18:29 +0200 Subject: [PATCH 67/85] Fix bug with the hardcoded configuration_url (asuswrt) (#151858) --- homeassistant/components/asuswrt/bridge.py | 7 +++++++ homeassistant/components/asuswrt/router.py | 2 +- tests/components/asuswrt/conftest.py | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index 3e3e372108b9..ee7df3ed7b81 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -120,11 +120,17 @@ class AsusWrtBridge(ABC): def __init__(self, host: str) -> None: """Initialize Bridge.""" + self._configuration_url = f"http://{host}" self._host = host self._firmware: str | None = None self._label_mac: str | None = None self._model: str | None = None + @property + def configuration_url(self) -> str: + """Return configuration URL.""" + return self._configuration_url + @property def host(self) -> str: """Return hostname.""" @@ -359,6 +365,7 @@ class AsusWrtHttpBridge(AsusWrtBridge): # get main router properties if mac := _identity.mac: self._label_mac = format_mac(mac) + self._configuration_url = self._api.webpanel self._firmware = str(_identity.firmware) self._model = _identity.model diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index c777535e242a..01c83dfc3ee0 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -388,11 +388,11 @@ class AsusWrtRouter: def device_info(self) -> DeviceInfo: """Return the device information.""" info = DeviceInfo( + configuration_url=self._api.configuration_url, identifiers={(DOMAIN, self._entry.unique_id or "AsusWRT")}, name=self.host, model=self._api.model or "Asus Router", manufacturer="Asus", - configuration_url=f"http://{self.host}", ) if self._api.firmware: info["sw_version"] = self._api.firmware diff --git a/tests/components/asuswrt/conftest.py b/tests/components/asuswrt/conftest.py index 95c8f3dbf747..3741aa44559e 100644 --- a/tests/components/asuswrt/conftest.py +++ b/tests/components/asuswrt/conftest.py @@ -12,6 +12,7 @@ import pytest from .common import ( ASUSWRT_BASE, + HOST, MOCK_MACS, PROTOCOL_HTTP, PROTOCOL_SSH, @@ -155,6 +156,9 @@ def mock_controller_connect_http(mock_devices_http): # Simulate connection status instance.connected = True + # Set the webpanel address + instance.webpanel = f"http://{HOST}:80" + # Identity instance.async_get_identity.return_value = AsusDevice( mac=ROUTER_MAC_ADDR, From 8d8e008123b700c845f04ad911cc7e0350f238b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 12:24:05 -0500 Subject: [PATCH 68/85] Fix HomeKit Controller overwhelming resource-limited devices by batching characteristic polling (#152209) --- .../homekit_controller/connection.py | 55 +++++++---- .../homekit_controller/test_connection.py | 96 ++++++++++++++++++- 2 files changed, 127 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index ce8dc498d6d5..e20842d186f9 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -57,7 +57,10 @@ from .utils import IidTuple, unique_id_to_iids RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 - +# HomeKit accessories have varying limits on how many characteristics +# they can handle per request. Since we don't know each device's specific limit, +# we batch requests to a conservative size to avoid overwhelming any device. +MAX_CHARACTERISTICS_PER_REQUEST = 49 BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds @@ -326,16 +329,20 @@ class HKDevice: ) entry.async_on_unload(self._async_cancel_subscription_timer) + if transport != Transport.BLE: + # Although async_populate_accessories_state fetched the accessory database, + # the /accessories endpoint may return cached values from the accessory's + # perspective. For example, Ecobee thermostats may report stale temperature + # values (like 100°C) in their /accessories response after restarting. + # We need to explicitly poll characteristics to get fresh sensor readings + # before processing the entity map and creating devices. + # Use poll_all=True since entities haven't registered their characteristics yet. + await self.async_update(poll_all=True) + await self.async_process_entity_map() if transport != Transport.BLE: - # When Home Assistant starts, we restore the accessory map from storage - # which contains characteristic values from when HA was last running. - # These values are stale and may be incorrect (e.g., Ecobee thermostats - # report 100°C when restarting). We need to poll for fresh values before - # creating entities. Use poll_all=True since entities haven't registered - # their characteristics yet. - await self.async_update(poll_all=True) + # Start regular polling after entity map is processed self._async_start_polling() # If everything is up to date, we can create the entities @@ -938,20 +945,26 @@ class HKDevice: async with self._polling_lock: _LOGGER.debug("Starting HomeKit device update: %s", self.unique_id) - try: - new_values_dict = await self.get_characteristics(to_poll) - except AccessoryNotFoundError: - # Not only did the connection fail, but also the accessory is not - # visible on the network. - self.async_set_available_state(False) - return - except (AccessoryDisconnectedError, EncryptionError): - # Temporary connection failure. Device may still available but our - # connection was dropped or we are reconnecting - self._poll_failures += 1 - if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE: + new_values_dict: dict[tuple[int, int], dict[str, Any]] = {} + to_poll_list = list(to_poll) + + for i in range(0, len(to_poll_list), MAX_CHARACTERISTICS_PER_REQUEST): + batch = to_poll_list[i : i + MAX_CHARACTERISTICS_PER_REQUEST] + try: + batch_values = await self.get_characteristics(batch) + new_values_dict.update(batch_values) + except AccessoryNotFoundError: + # Not only did the connection fail, but also the accessory is not + # visible on the network. self.async_set_available_state(False) - return + return + except (AccessoryDisconnectedError, EncryptionError): + # Temporary connection failure. Device may still available but our + # connection was dropped or we are reconnecting + self._poll_failures += 1 + if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE: + self.async_set_available_state(False) + return self._poll_failures = 0 self.process_new_events(new_values_dict) diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 99203d400fea..6c5ccdfd8b0a 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -13,6 +13,9 @@ from aiohomekit.testing import FakeController import pytest from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE +from homeassistant.components.homekit_controller.connection import ( + MAX_CHARACTERISTICS_PER_REQUEST, +) from homeassistant.components.homekit_controller.const import ( DEBOUNCE_COOLDOWN, DOMAIN, @@ -377,9 +380,15 @@ async def test_poll_firmware_version_only_all_watchable_accessory_mode( state = await helper.poll_and_get_state() assert state.state == STATE_OFF assert mock_get_characteristics.call_count == 2 - # Verify everything is polled - assert mock_get_characteristics.call_args_list[0][0][0] == {(1, 10), (1, 11)} - assert mock_get_characteristics.call_args_list[1][0][0] == {(1, 10), (1, 11)} + # Verify everything is polled (convert to set for comparison since batching changes the type) + assert set(mock_get_characteristics.call_args_list[0][0][0]) == { + (1, 10), + (1, 11), + } + assert set(mock_get_characteristics.call_args_list[1][0][0]) == { + (1, 10), + (1, 11), + } # Test device goes offline helper.pairing.available = False @@ -526,3 +535,84 @@ async def test_poll_all_on_startup_refreshes_stale_values( state = hass.states.get("climate.homew") assert state is not None assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + +async def test_characteristic_polling_batching( + hass: HomeAssistant, get_next_aid: Callable[[], int] +) -> None: + """Test that characteristic polling is batched to MAX_CHARACTERISTICS_PER_REQUEST.""" + + # Create a large accessory with many characteristics (more than 49) + def create_large_accessory_with_many_chars(accessory: Accessory) -> None: + """Create an accessory with many characteristics to test batching.""" + # Add multiple services with many characteristics each + for service_num in range(10): # 10 services + service = accessory.add_service( + ServicesTypes.LIGHTBULB, name=f"Light {service_num}" + ) + # Each lightbulb service gets several characteristics + service.add_char(CharacteristicsTypes.ON) + service.add_char(CharacteristicsTypes.BRIGHTNESS) + service.add_char(CharacteristicsTypes.HUE) + service.add_char(CharacteristicsTypes.SATURATION) + service.add_char(CharacteristicsTypes.COLOR_TEMPERATURE) + # Set initial values + for char in service.characteristics: + if char.type != CharacteristicsTypes.IDENTIFY: + char.value = 0 + + helper = await setup_test_component( + hass, get_next_aid(), create_large_accessory_with_many_chars + ) + + # Track the get_characteristics calls + get_chars_calls = [] + original_get_chars = helper.pairing.get_characteristics + + async def mock_get_characteristics(chars): + """Mock get_characteristics to track batch sizes.""" + get_chars_calls.append(list(chars)) + return await original_get_chars(chars) + + # Clear any calls from setup + get_chars_calls.clear() + + # Patch get_characteristics to track calls + with mock.patch.object( + helper.pairing, "get_characteristics", side_effect=mock_get_characteristics + ): + # Trigger an update through time_changed which simulates regular polling + # time_changed expects seconds, not a datetime + await time_changed(hass, 300) # 5 minutes in seconds + await hass.async_block_till_done() + + # We created 10 lightbulb services with 5 characteristics each = 50 total + # Plus any base accessory characteristics that are pollable + # This should result in exactly 2 batches + assert len(get_chars_calls) == 2, ( + f"Should have made exactly 2 batched calls, got {len(get_chars_calls)}" + ) + + # Check that no batch exceeded MAX_CHARACTERISTICS_PER_REQUEST + for i, batch in enumerate(get_chars_calls): + assert len(batch) <= MAX_CHARACTERISTICS_PER_REQUEST, ( + f"Batch {i} size {len(batch)} exceeded maximum {MAX_CHARACTERISTICS_PER_REQUEST}" + ) + + # Verify the total number of characteristics polled + total_chars = sum(len(batch) for batch in get_chars_calls) + # Each lightbulb has: ON, BRIGHTNESS, HUE, SATURATION, COLOR_TEMPERATURE = 5 + # 10 lightbulbs = 50 characteristics + assert total_chars == 50, ( + f"Should have polled exactly 50 characteristics, got {total_chars}" + ) + + # The first batch should be full (49 characteristics) + assert len(get_chars_calls[0]) == 49, ( + f"First batch should have exactly 49 characteristics, got {len(get_chars_calls[0])}" + ) + + # The second batch should have exactly 1 characteristic + assert len(get_chars_calls[1]) == 1, ( + f"Second batch should have exactly 1 characteristic, got {len(get_chars_calls[1])}" + ) From c87dba878dea9636ad958be406f4576fb02914ee Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Sat, 13 Sep 2025 10:36:15 -0400 Subject: [PATCH 69/85] Upgrade waterfurnace to 1.2.0 (#152241) --- homeassistant/components/waterfurnace/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index 2bf72acb047b..98d21dd9425e 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -6,5 +6,5 @@ "iot_class": "cloud_polling", "loggers": ["waterfurnace"], "quality_scale": "legacy", - "requirements": ["waterfurnace==1.1.0"] + "requirements": ["waterfurnace==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 89d03562f653..a14a56f56cb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3090,7 +3090,7 @@ wallbox==0.9.0 watchdog==6.0.0 # homeassistant.components.waterfurnace -waterfurnace==1.1.0 +waterfurnace==1.2.0 # homeassistant.components.watergate watergate-local-api==2024.4.1 From 54859e8a83f1bb50117d6b7dc4378dada65bca01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 12:44:34 -0500 Subject: [PATCH 70/85] Bump aiohomekit to 3.2.16 (#152255) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index d15479aa9d5b..ef4fdadb24c4 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.15"], + "requirements": ["aiohomekit==3.2.16"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index a14a56f56cb9..7e0de86a8d87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -271,7 +271,7 @@ aiohasupervisor==0.3.2 aiohomeconnect==0.19.0 # homeassistant.components.homekit_controller -aiohomekit==3.2.15 +aiohomekit==3.2.16 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89d9b6d11a8f..24aeff1a21a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -256,7 +256,7 @@ aiohasupervisor==0.3.2 aiohomeconnect==0.19.0 # homeassistant.components.homekit_controller -aiohomekit==3.2.15 +aiohomekit==3.2.16 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 From 265f5da21a11a6ca4a896655385540ff8f2a94f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 13:19:37 -0500 Subject: [PATCH 71/85] Bump bluetooth-auto-recovery to 1.5.3 (#152256) --- 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 431ec10b366d..bf5345e0ba47 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak==1.0.1", "bleak-retry-connector==4.4.3", "bluetooth-adapters==2.1.0", - "bluetooth-auto-recovery==1.5.2", + "bluetooth-auto-recovery==1.5.3", "bluetooth-data-tools==1.28.2", "dbus-fast==2.44.3", "habluetooth==5.6.4" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7da074c28eaa..cf2451e9d14e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ bcrypt==4.3.0 bleak-retry-connector==4.4.3 bleak==1.0.1 bluetooth-adapters==2.1.0 -bluetooth-auto-recovery==1.5.2 +bluetooth-auto-recovery==1.5.3 bluetooth-data-tools==1.28.2 cached-ipaddress==0.10.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index 7e0de86a8d87..929ed6c53932 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ bluemaestro-ble==0.4.1 bluetooth-adapters==2.1.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.5.2 +bluetooth-auto-recovery==1.5.3 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24aeff1a21a8..e6c68c20fc9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -586,7 +586,7 @@ bluemaestro-ble==0.4.1 bluetooth-adapters==2.1.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.5.2 +bluetooth-auto-recovery==1.5.3 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble From bbb67db3546557d2464128a613a6ea52756e77b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sun, 14 Sep 2025 19:47:01 +0200 Subject: [PATCH 72/85] Add proper error handling for /actions endpoint for miele (#152290) --- homeassistant/components/miele/coordinator.py | 20 +++++++++++-- tests/components/miele/test_init.py | 28 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index 98f5c9f8b1cf..b3eb1185bd1a 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -2,12 +2,13 @@ from __future__ import annotations -import asyncio.timeouts +import asyncio from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging +from aiohttp import ClientResponseError from pymiele import MieleAction, MieleAPI, MieleDevice from homeassistant.config_entries import ConfigEntry @@ -66,7 +67,22 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]): self.devices = devices actions = {} for device_id in devices: - actions_json = await self.api.get_actions(device_id) + try: + actions_json = await self.api.get_actions(device_id) + except ClientResponseError as err: + _LOGGER.debug( + "Error fetching actions for device %s: Status: %s, Message: %s", + device_id, + err.status, + err.message, + ) + actions_json = {} + except TimeoutError: + _LOGGER.debug( + "Timeout fetching actions for device %s", + device_id, + ) + actions_json = {} actions[device_id] = MieleAction(actions_json) return MieleCoordinatorData(devices=devices, actions=actions) diff --git a/tests/components/miele/test_init.py b/tests/components/miele/test_init.py index cdf1a39b4213..0448096a1157 100644 --- a/tests/components/miele/test_init.py +++ b/tests/components/miele/test_init.py @@ -5,7 +5,7 @@ import http import time from unittest.mock import MagicMock -from aiohttp import ClientConnectionError +from aiohttp import ClientConnectionError, ClientResponseError from freezegun.api import FrozenDateTimeFactory from pymiele import OAUTH2_TOKEN import pytest @@ -210,3 +210,29 @@ async def test_setup_all_platforms( # Check a sample sensor for each new device assert hass.states.get("sensor.dishwasher").state == "in_use" assert hass.states.get("sensor.oven_temperature_2").state == "175.0" + + +@pytest.mark.parametrize( + "side_effect", + [ + ClientResponseError("test", "Test"), + TimeoutError, + ], + ids=[ + "ClientResponseError", + "TimeoutError", + ], +) +async def test_load_entry_with_action_error( + hass: HomeAssistant, + mock_miele_client: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, +) -> None: + """Test load with error from actions endpoint.""" + mock_miele_client.get_actions.side_effect = side_effect + await setup_integration(hass, mock_config_entry) + entry = mock_config_entry + + assert entry.state is ConfigEntryState.LOADED + assert mock_miele_client.get_actions.call_count == 5 From 8728312e87ff53cd66fcef6d7c3c6fe5ca8c4126 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Sep 2025 12:02:11 -0500 Subject: [PATCH 73/85] Bump aiohomekit to 3.2.17 (#152297) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ef4fdadb24c4..e9ea92c78e82 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.16"], + "requirements": ["aiohomekit==3.2.17"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 929ed6c53932..bbfc8b05ad90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -271,7 +271,7 @@ aiohasupervisor==0.3.2 aiohomeconnect==0.19.0 # homeassistant.components.homekit_controller -aiohomekit==3.2.16 +aiohomekit==3.2.17 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6c68c20fc9e..aa869b5e8502 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -256,7 +256,7 @@ aiohasupervisor==0.3.2 aiohomeconnect==0.19.0 # homeassistant.components.homekit_controller -aiohomekit==3.2.16 +aiohomekit==3.2.17 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 From 0b159bdb9c03f92ee464791005bcb7966bf4e271 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 14 Sep 2025 14:07:31 -0700 Subject: [PATCH 74/85] Update authorization server to prefer absolute urls (#152313) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/auth/login_flow.py | 19 +++++++-- tests/components/auth/test_login_flow.py | 46 ++++++++++++++++++--- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 69ae3eb65bd4..675c2d10fea3 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -92,7 +92,11 @@ from homeassistant.components.http.ban import ( from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.network import is_cloud_connection +from homeassistant.helpers.network import ( + NoURLAvailableError, + get_url, + is_cloud_connection, +) from homeassistant.util.network import is_local from . import indieauth @@ -125,11 +129,18 @@ class WellKnownOAuthInfoView(HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Return the well known OAuth2 authorization info.""" + hass = request.app[KEY_HASS] + # Some applications require absolute urls, so we prefer using the + # current requests url if possible, with fallback to a relative url. + try: + url_prefix = get_url(hass, require_current_request=True) + except NoURLAvailableError: + url_prefix = "" return self.json( { - "authorization_endpoint": "/auth/authorize", - "token_endpoint": "/auth/token", - "revocation_endpoint": "/auth/revoke", + "authorization_endpoint": f"{url_prefix}/auth/authorize", + "token_endpoint": f"{url_prefix}/auth/token", + "revocation_endpoint": f"{url_prefix}/auth/revoke", "response_types_supported": ["code"], "service_documentation": ( "https://developers.home-assistant.io/docs/auth_api" diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index af9a2cf62f1a..f7d20687c926 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -7,6 +7,7 @@ from unittest.mock import patch import pytest from homeassistant.core import HomeAssistant +from homeassistant.core_config import async_process_ha_core_config from . import BASE_CONFIG, async_setup_auth @@ -371,19 +372,54 @@ async def test_login_exist_user_ip_changes( assert response == {"message": "IP address changed"} +@pytest.mark.usefixtures("current_request_with_host") # Has example.com host +@pytest.mark.parametrize( + ("config", "expected_url_prefix"), + [ + ( + { + "internal_url": "http://192.168.1.100:8123", + # Current request matches external url + "external_url": "https://example.com", + }, + "https://example.com", + ), + ( + { + # Current request matches internal url + "internal_url": "https://example.com", + "external_url": "https://other.com", + }, + "https://example.com", + ), + ( + { + # Current request does not match either url + "internal_url": "https://other.com", + "external_url": "https://again.com", + }, + "", + ), + ], + ids=["external_url", "internal_url", "no_match"], +) async def test_well_known_auth_info( - hass: HomeAssistant, aiohttp_client: ClientSessionGenerator + hass: HomeAssistant, + aiohttp_client: ClientSessionGenerator, + config: dict[str, str], + expected_url_prefix: str, ) -> None: - """Test logging in and the ip address changes results in an rejection.""" + """Test the well-known OAuth authorization server endpoint with different URL configurations.""" + await async_process_ha_core_config(hass, config) client = await async_setup_auth(hass, aiohttp_client, setup_api=True) resp = await client.get( "/.well-known/oauth-authorization-server", ) assert resp.status == 200 assert await resp.json() == { - "authorization_endpoint": "/auth/authorize", - "token_endpoint": "/auth/token", - "revocation_endpoint": "/auth/revoke", + "authorization_endpoint": f"{expected_url_prefix}/auth/authorize", + "token_endpoint": f"{expected_url_prefix}/auth/token", + "revocation_endpoint": f"{expected_url_prefix}/auth/revoke", "response_types_supported": ["code"], "service_documentation": "https://developers.home-assistant.io/docs/auth_api", } From 757aec1c6b26414811b857899b0390addbe4b3ef Mon Sep 17 00:00:00 2001 From: Imeon-Energy Date: Mon, 15 Sep 2025 11:14:31 +0200 Subject: [PATCH 75/85] Bump imeon_inverter_api to 0.4.0 (#152351) Co-authored-by: TheBushBoy --- homeassistant/components/imeon_inverter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imeon_inverter/manifest.json b/homeassistant/components/imeon_inverter/manifest.json index 837b73512417..ed24d169d633 100644 --- a/homeassistant/components/imeon_inverter/manifest.json +++ b/homeassistant/components/imeon_inverter/manifest.json @@ -7,7 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["imeon_inverter_api==0.3.16"], + "requirements": ["imeon_inverter_api==0.4.0"], "ssdp": [ { "manufacturer": "IMEON", diff --git a/requirements_all.txt b/requirements_all.txt index bbfc8b05ad90..e40e6a9891d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ igloohome-api==0.1.1 ihcsdk==2.8.5 # homeassistant.components.imeon_inverter -imeon_inverter_api==0.3.16 +imeon_inverter_api==0.4.0 # homeassistant.components.imgw_pib imgw_pib==1.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa869b5e8502..a956ae28415c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1075,7 +1075,7 @@ ifaddr==0.2.0 igloohome-api==0.1.1 # homeassistant.components.imeon_inverter -imeon_inverter_api==0.3.16 +imeon_inverter_api==0.4.0 # homeassistant.components.imgw_pib imgw_pib==1.5.4 From 10b186a20de3eab026863ed813b40ea1e36c8cfa Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 16 Sep 2025 10:08:08 +0200 Subject: [PATCH 76/85] Bump pylamarzocco to 2.1.0 (#152364) --- .../components/lamarzocco/__init__.py | 41 ++++++------- .../components/lamarzocco/config_flow.py | 13 +++- homeassistant/components/lamarzocco/const.py | 1 + .../components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lamarzocco/__init__.py | 3 + tests/components/lamarzocco/conftest.py | 24 +++++++- .../components/lamarzocco/test_config_flow.py | 17 +++++- tests/components/lamarzocco/test_init.py | 60 +++++++++---------- 10 files changed, 102 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 92184b4ac51c..15ff16346879 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +import uuid from packaging import version from pylamarzocco import ( @@ -11,6 +12,7 @@ from pylamarzocco import ( ) from pylamarzocco.const import FirmwareType from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful +from pylamarzocco.util import InstallationKey, generate_installation_key from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.const import ( @@ -25,7 +27,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import CONF_USE_BLUETOOTH, DOMAIN +from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN from .coordinator import ( LaMarzoccoConfigEntry, LaMarzoccoConfigUpdateCoordinator, @@ -60,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + installation_key=InstallationKey.from_json(entry.data[CONF_INSTALLATION_KEY]), client=async_create_clientsession(hass), ) @@ -166,45 +169,37 @@ async def async_migrate_entry( hass: HomeAssistant, entry: LaMarzoccoConfigEntry ) -> bool: """Migrate config entry.""" - if entry.version > 3: + if entry.version > 4: # guard against downgrade from a future version return False - if entry.version == 1: + if entry.version in (1, 2): _LOGGER.error( - "Migration from version 1 is no longer supported, please remove and re-add the integration" + "Migration from version 1 or 2 is no longer supported, please remove and re-add the integration" ) return False - if entry.version == 2: + if entry.version == 3: + installation_key = generate_installation_key(str(uuid.uuid4()).lower()) cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + installation_key=installation_key, ) try: - things = await cloud_client.list_things() + await cloud_client.async_register_client() except (AuthFail, RequestNotSuccessful) as exc: _LOGGER.error("Migration failed with error %s", exc) return False - v3_data = { - CONF_USERNAME: entry.data[CONF_USERNAME], - CONF_PASSWORD: entry.data[CONF_PASSWORD], - CONF_TOKEN: next( - ( - thing.ble_auth_token - for thing in things - if thing.serial_number == entry.unique_id - ), - None, - ), - } - if CONF_MAC in entry.data: - v3_data[CONF_MAC] = entry.data[CONF_MAC] + hass.config_entries.async_update_entry( entry, - data=v3_data, - version=3, + data={ + **entry.data, + CONF_INSTALLATION_KEY: installation_key.to_json(), + }, + version=4, ) - _LOGGER.debug("Migrated La Marzocco config entry to version 2") + _LOGGER.debug("Migrated La Marzocco config entry to version 4") return True diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index fb968a0b4af9..7f08ac9a48ed 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -5,11 +5,13 @@ from __future__ import annotations from collections.abc import Mapping import logging from typing import Any +import uuid from aiohttp import ClientSession from pylamarzocco import LaMarzoccoCloudClient from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful from pylamarzocco.models import Thing +from pylamarzocco.util import InstallationKey, generate_installation_key import voluptuous as vol from homeassistant.components.bluetooth import ( @@ -45,7 +47,7 @@ from homeassistant.helpers.selector import ( ) from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo -from .const import CONF_USE_BLUETOOTH, DOMAIN +from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN from .coordinator import LaMarzoccoConfigEntry CONF_MACHINE = "machine" @@ -57,9 +59,10 @@ _LOGGER = logging.getLogger(__name__) class LmConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for La Marzocco.""" - VERSION = 3 + VERSION = 4 _client: ClientSession + _installation_key: InstallationKey def __init__(self) -> None: """Initialize the config flow.""" @@ -84,12 +87,17 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): } self._client = async_create_clientsession(self.hass) + self._installation_key = generate_installation_key( + str(uuid.uuid4()).lower() + ) cloud_client = LaMarzoccoCloudClient( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], client=self._client, + installation_key=self._installation_key, ) try: + await cloud_client.async_register_client() things = await cloud_client.list_things() except AuthFail: _LOGGER.debug("Server rejected login credentials") @@ -184,6 +192,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): title=selected_device.name, data={ **self._config, + CONF_INSTALLATION_KEY: self._installation_key.to_json(), CONF_TOKEN: self._things[serial_number].ble_auth_token, }, ) diff --git a/homeassistant/components/lamarzocco/const.py b/homeassistant/components/lamarzocco/const.py index 57db84f94da1..680557d85f18 100644 --- a/homeassistant/components/lamarzocco/const.py +++ b/homeassistant/components/lamarzocco/const.py @@ -5,3 +5,4 @@ from typing import Final DOMAIN: Final = "lamarzocco" CONF_USE_BLUETOOTH: Final = "use_bluetooth" +CONF_INSTALLATION_KEY: Final = "installation_key" diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 3c070769b5b0..ec55a7e8c2b1 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.11"] + "requirements": ["pylamarzocco==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e40e6a9891d7..b7e231f3be21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2103,7 +2103,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.11 +pylamarzocco==2.1.0 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a956ae28415c..5fd585f3e03a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1748,7 +1748,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.11 +pylamarzocco==2.1.0 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/tests/components/lamarzocco/__init__.py b/tests/components/lamarzocco/__init__.py index 80493aa83c97..55335f720c3a 100644 --- a/tests/components/lamarzocco/__init__.py +++ b/tests/components/lamarzocco/__init__.py @@ -54,3 +54,6 @@ def get_bluetooth_service_info(model: ModelName, serial: str) -> BluetoothServic service_uuids=[], source="local", ) + + +MOCK_INSTALLATION_KEY = '{"secret": "K9ZW2vlMSb3QXmhySx4pxAbTHujWj3VZ01Jn3D/sO98=", "private_key": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8iotE8El786F6kHuEL8GyYhjDB7oo06vNhQwtewF37yhRANCAAQCLb9lHskiavvfkI4H2B+WsdkusfgBBFuFNRrGV8bqPMra1TK5myb/ecdZfHJBBJrcbdt90QMDmXQm5L3muXXe", "installation_id": "4e966f3f-2abc-49c4-a362-3cd3346f1a87"}' diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index ad1378a6dc19..7907a1d6a7ed 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -12,13 +12,14 @@ from pylamarzocco.models import ( ThingSettings, ThingStatistics, ) +from pylamarzocco.util import InstallationKey import pytest -from homeassistant.components.lamarzocco.const import DOMAIN +from homeassistant.components.lamarzocco.const import CONF_INSTALLATION_KEY, DOMAIN from homeassistant.const import CONF_ADDRESS, CONF_TOKEN from homeassistant.core import HomeAssistant -from . import SERIAL_DICT, USER_INPUT, async_init_integration +from . import MOCK_INSTALLATION_KEY, SERIAL_DICT, USER_INPUT, async_init_integration from tests.common import MockConfigEntry, load_json_object_fixture @@ -31,11 +32,12 @@ def mock_config_entry( return MockConfigEntry( title="My LaMarzocco", domain=DOMAIN, - version=3, + version=4, data=USER_INPUT | { CONF_ADDRESS: "000000000000", CONF_TOKEN: "token", + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, }, unique_id=mock_lamarzocco.serial_number, ) @@ -51,6 +53,22 @@ async def init_integration( return mock_config_entry +@pytest.fixture(autouse=True) +def mock_generate_installation_key() -> Generator[MagicMock]: + """Return a mocked generate_installation_key.""" + with ( + patch( + "homeassistant.components.lamarzocco.generate_installation_key", + return_value=InstallationKey.from_json(MOCK_INSTALLATION_KEY), + ) as mock_generate, + patch( + "homeassistant.components.lamarzocco.config_flow.generate_installation_key", + new=mock_generate, + ), + ): + yield mock_generate + + @pytest.fixture def device_fixture() -> ModelName: """Return the device fixture for a specific device.""" diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index e50707f71afd..5d0a514b7932 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -9,7 +9,11 @@ from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful import pytest from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE -from homeassistant.components.lamarzocco.const import CONF_USE_BLUETOOTH, DOMAIN +from homeassistant.components.lamarzocco.const import ( + CONF_INSTALLATION_KEY, + CONF_USE_BLUETOOTH, + DOMAIN, +) from homeassistant.config_entries import ( SOURCE_BLUETOOTH, SOURCE_DHCP, @@ -23,7 +27,12 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo -from . import USER_INPUT, async_init_integration, get_bluetooth_service_info +from . import ( + MOCK_INSTALLATION_KEY, + USER_INPUT, + async_init_integration, + get_bluetooth_service_info, +) from tests.common import MockConfigEntry @@ -68,6 +77,7 @@ async def __do_sucessful_machine_selection_step( assert result["data"] == { **USER_INPUT, CONF_TOKEN: None, + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } assert result["result"].unique_id == "GS012345" @@ -344,6 +354,7 @@ async def test_bluetooth_discovery( **USER_INPUT, CONF_MAC: "aa:bb:cc:dd:ee:ff", CONF_TOKEN: "dummyToken", + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } @@ -407,6 +418,7 @@ async def test_bluetooth_discovery_errors( **USER_INPUT, CONF_MAC: "aa:bb:cc:dd:ee:ff", CONF_TOKEN: None, + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } @@ -438,6 +450,7 @@ async def test_dhcp_discovery( **USER_INPUT, CONF_ADDRESS: "aabbccddeeff", CONF_TOKEN: None, + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index 1e56e540e2a6..e6bf4a0af62e 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -8,15 +8,11 @@ from pylamarzocco.models import WebSocketDetails import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE -from homeassistant.components.lamarzocco.const import DOMAIN +from homeassistant.components.lamarzocco.const import CONF_INSTALLATION_KEY, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_ADDRESS, - CONF_HOST, CONF_MAC, - CONF_MODEL, - CONF_NAME, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, ) @@ -27,7 +23,12 @@ from homeassistant.helpers import ( issue_registry as ir, ) -from . import USER_INPUT, async_init_integration, get_bluetooth_service_info +from . import ( + MOCK_INSTALLATION_KEY, + USER_INPUT, + async_init_integration, + get_bluetooth_service_info, +) from tests.common import MockConfigEntry @@ -129,66 +130,65 @@ async def test_v1_migration_fails( assert entry_v1.state is ConfigEntryState.MIGRATION_ERROR -async def test_v2_migration( +async def test_v4_migration( hass: HomeAssistant, mock_lamarzocco: MagicMock, ) -> None: - """Test v2 -> v3 Migration.""" + """Test v3 -> v4 Migration.""" - entry_v2 = MockConfigEntry( + entry_v3 = MockConfigEntry( domain=DOMAIN, - version=2, + version=3, unique_id=mock_lamarzocco.serial_number, data={ **USER_INPUT, - CONF_HOST: "192.168.1.24", - CONF_NAME: "La Marzocco", - CONF_MODEL: ModelName.GS3_MP.value, - CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_ADDRESS: "000000000000", + CONF_TOKEN: "token", }, ) - entry_v2.add_to_hass(hass) + entry_v3.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry_v2.entry_id) - assert entry_v2.state is ConfigEntryState.LOADED - assert entry_v2.version == 3 - assert dict(entry_v2.data) == { + assert await hass.config_entries.async_setup(entry_v3.entry_id) + assert entry_v3.state is ConfigEntryState.LOADED + assert entry_v3.version == 4 + assert dict(entry_v3.data) == { **USER_INPUT, - CONF_MAC: "aa:bb:cc:dd:ee:ff", - CONF_TOKEN: None, + CONF_ADDRESS: "000000000000", + CONF_TOKEN: "token", + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } async def test_migration_errors( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, mock_cloud_client: MagicMock, mock_lamarzocco: MagicMock, ) -> None: """Test errors during migration.""" - mock_cloud_client.list_things.side_effect = RequestNotSuccessful("Error") + mock_cloud_client.async_register_client.side_effect = RequestNotSuccessful("Error") - entry_v2 = MockConfigEntry( + entry_v3 = MockConfigEntry( domain=DOMAIN, - version=2, + version=3, unique_id=mock_lamarzocco.serial_number, data={ **USER_INPUT, - CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_ADDRESS: "000000000000", + CONF_TOKEN: "token", }, ) - entry_v2.add_to_hass(hass) + entry_v3.add_to_hass(hass) - assert not await hass.config_entries.async_setup(entry_v2.entry_id) - assert entry_v2.state is ConfigEntryState.MIGRATION_ERROR + assert not await hass.config_entries.async_setup(entry_v3.entry_id) + assert entry_v3.state is ConfigEntryState.MIGRATION_ERROR async def test_config_flow_entry_migration_downgrade( hass: HomeAssistant, ) -> None: """Test that config entry fails setup if the version is from the future.""" - entry = MockConfigEntry(domain=DOMAIN, version=4) + entry = MockConfigEntry(domain=DOMAIN, version=5) entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(entry.entry_id) From 9cd940b7df857b36e02a682cf6ac082b389e13d5 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 16 Sep 2025 12:15:57 +0200 Subject: [PATCH 77/85] Add La Marzocco specific client headers (#152419) --- homeassistant/components/lamarzocco/__init__.py | 17 ++++++++++++++++- .../components/lamarzocco/config_flow.py | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 15ff16346879..96d4f4c61ac4 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -4,6 +4,7 @@ import asyncio import logging import uuid +from aiohttp import ClientSession from packaging import version from pylamarzocco import ( LaMarzoccoBluetoothClient, @@ -21,6 +22,7 @@ from homeassistant.const import ( CONF_TOKEN, CONF_USERNAME, Platform, + __version__, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -63,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], installation_key=InstallationKey.from_json(entry.data[CONF_INSTALLATION_KEY]), - client=async_create_clientsession(hass), + client=create_client_session(hass), ) try: @@ -185,6 +187,7 @@ async def async_migrate_entry( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], installation_key=installation_key, + client=create_client_session(hass), ) try: await cloud_client.async_register_client() @@ -203,3 +206,15 @@ async def async_migrate_entry( _LOGGER.debug("Migrated La Marzocco config entry to version 4") return True + + +def create_client_session(hass: HomeAssistant) -> ClientSession: + """Create a ClientSession with La Marzocco specific headers.""" + + return async_create_clientsession( + hass, + headers={ + "X-Client": "HOME_ASSISTANT", + "X-Client-Build": __version__, + }, + ) diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index 7f08ac9a48ed..ab99fbbc63fe 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -35,7 +35,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -47,6 +46,7 @@ from homeassistant.helpers.selector import ( ) from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from . import create_client_session from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN from .coordinator import LaMarzoccoConfigEntry @@ -86,7 +86,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): **user_input, } - self._client = async_create_clientsession(self.hass) + self._client = create_client_session(self.hass) self._installation_key = generate_installation_key( str(uuid.uuid4()).lower() ) From 950e758b62b460ec40cf95b3461c4d465044e3bc Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 16 Sep 2025 19:39:39 +0200 Subject: [PATCH 78/85] Fix KNX UI schema missing DPT (#152430) --- .../knx/storage/entity_store_schema.py | 22 +++++---- .../knx/snapshots/test_websocket.ambr | 48 +++++++++++++++++-- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py index 21252e35f3a7..934008132a89 100644 --- a/homeassistant/components/knx/storage/entity_store_schema.py +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -118,27 +118,31 @@ COVER_KNX_SCHEMA = AllSerializeFirst( vol.Schema( { "section_binary_control": KNXSectionFlat(), - vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False), + vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False, valid_dpt="1"), vol.Optional(CoverConf.INVERT_UPDOWN): selector.BooleanSelector(), "section_stop_control": KNXSectionFlat(), - vol.Optional(CONF_GA_STOP): GASelector(state=False), - vol.Optional(CONF_GA_STEP): GASelector(state=False), + vol.Optional(CONF_GA_STOP): GASelector(state=False, valid_dpt="1"), + vol.Optional(CONF_GA_STEP): GASelector(state=False, valid_dpt="1"), "section_position_control": KNXSectionFlat(collapsible=True), - vol.Optional(CONF_GA_POSITION_SET): GASelector(state=False), - vol.Optional(CONF_GA_POSITION_STATE): GASelector(write=False), + vol.Optional(CONF_GA_POSITION_SET): GASelector( + state=False, valid_dpt="5.001" + ), + vol.Optional(CONF_GA_POSITION_STATE): GASelector( + write=False, valid_dpt="5.001" + ), vol.Optional(CoverConf.INVERT_POSITION): selector.BooleanSelector(), "section_tilt_control": KNXSectionFlat(collapsible=True), - vol.Optional(CONF_GA_ANGLE): GASelector(), + vol.Optional(CONF_GA_ANGLE): GASelector(valid_dpt="5.001"), vol.Optional(CoverConf.INVERT_ANGLE): selector.BooleanSelector(), "section_travel_time": KNXSectionFlat(), - vol.Optional( + vol.Required( CoverConf.TRAVELLING_TIME_UP, default=25 ): selector.NumberSelector( selector.NumberSelectorConfig( min=0, max=1000, step=0.1, unit_of_measurement="s" ) ), - vol.Optional( + vol.Required( CoverConf.TRAVELLING_TIME_DOWN, default=25 ): selector.NumberSelector( selector.NumberSelectorConfig( @@ -310,7 +314,7 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst( SWITCH_KNX_SCHEMA = vol.Schema( { "section_switch": KNXSectionFlat(), - vol.Required(CONF_GA_SWITCH): GASelector(write_required=True), + vol.Required(CONF_GA_SWITCH): GASelector(write_required=True, valid_dpt="1"), vol.Optional(CONF_INVERT, default=False): selector.BooleanSelector(), vol.Optional(CONF_RESPOND_TO_READ, default=False): selector.BooleanSelector(), vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(), diff --git a/tests/components/knx/snapshots/test_websocket.ambr b/tests/components/knx/snapshots/test_websocket.ambr index 6dc651195aeb..388c68e0d3f4 100644 --- a/tests/components/knx/snapshots/test_websocket.ambr +++ b/tests/components/knx/snapshots/test_websocket.ambr @@ -111,6 +111,12 @@ 'options': dict({ 'passive': True, 'state': False, + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), 'write': dict({ 'required': False, }), @@ -140,6 +146,12 @@ 'options': dict({ 'passive': True, 'state': False, + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), 'write': dict({ 'required': False, }), @@ -153,6 +165,12 @@ 'options': dict({ 'passive': True, 'state': False, + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), 'write': dict({ 'required': False, }), @@ -172,6 +190,12 @@ 'options': dict({ 'passive': True, 'state': False, + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), 'write': dict({ 'required': False, }), @@ -187,6 +211,12 @@ 'state': dict({ 'required': False, }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), 'write': False, }), 'required': False, @@ -216,6 +246,12 @@ 'state': dict({ 'required': False, }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), 'write': dict({ 'required': False, }), @@ -242,8 +278,7 @@ dict({ 'default': 25, 'name': 'travelling_time_up', - 'optional': True, - 'required': False, + 'required': True, 'selector': dict({ 'number': dict({ 'max': 1000.0, @@ -258,8 +293,7 @@ dict({ 'default': 25, 'name': 'travelling_time_down', - 'optional': True, - 'required': False, + 'required': True, 'selector': dict({ 'number': dict({ 'max': 1000.0, @@ -746,6 +780,12 @@ 'state': dict({ 'required': False, }), + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), 'write': dict({ 'required': True, }), From b37237d24bff9d74bbc54b7389a923c751ede9a8 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Tue, 16 Sep 2025 18:32:13 +0200 Subject: [PATCH 79/85] Bump pyemoncms to 0.1.3 (#152436) --- homeassistant/components/emoncms/manifest.json | 2 +- homeassistant/components/emoncms_history/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index bc86e6e9babd..d21da4539762 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emoncms", "iot_class": "local_polling", - "requirements": ["pyemoncms==0.1.2"] + "requirements": ["pyemoncms==0.1.3"] } diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index 3c8c445b766e..29a061f9229e 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/emoncms_history", "iot_class": "local_polling", "quality_scale": "legacy", - "requirements": ["pyemoncms==0.1.2"] + "requirements": ["pyemoncms==0.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b7e231f3be21..4f1c2d3c238e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1962,7 +1962,7 @@ pyegps==0.2.5 # homeassistant.components.emoncms # homeassistant.components.emoncms_history -pyemoncms==0.1.2 +pyemoncms==0.1.3 # homeassistant.components.enphase_envoy pyenphase==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5fd585f3e03a..5cedd3175c10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1637,7 +1637,7 @@ pyegps==0.2.5 # homeassistant.components.emoncms # homeassistant.components.emoncms_history -pyemoncms==0.1.2 +pyemoncms==0.1.3 # homeassistant.components.enphase_envoy pyenphase==2.3.0 From 8eee53036a04a8c666b74139a74cf894df70eecf Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 17 Sep 2025 08:39:28 -0400 Subject: [PATCH 80/85] Fix Sonos set_volume float precision issue (#152493) --- homeassistant/components/sonos/media_player.py | 2 +- tests/components/sonos/test_media_player.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 0b30c820da39..984996302d99 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -410,7 +410,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): @soco_error() def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - self.soco.volume = int(volume * 100) + self.soco.volume = int(round(volume * 100)) @soco_error(UPNP_ERRORS_TO_IGNORE) def set_shuffle(self, shuffle: bool) -> None: diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 41b18750fd4c..b7889edf416c 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1101,11 +1101,11 @@ async def test_volume( await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: "media_player.zone_a", ATTR_MEDIA_VOLUME_LEVEL: 0.30}, + {ATTR_ENTITY_ID: "media_player.zone_a", ATTR_MEDIA_VOLUME_LEVEL: 0.57}, blocking=True, ) # SoCo uses 0..100 for its range. - assert soco.volume == 30 + assert soco.volume == 57 @pytest.mark.parametrize( From cf907ae1968c89ed94336e4fc46678b02d6ba2f5 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 18 Sep 2025 04:29:27 -0700 Subject: [PATCH 81/85] Bump opower to 0.15.5 (#152531) --- 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 dc69c33cd5d4..251734107abc 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.4"] + "requirements": ["opower==0.15.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4f1c2d3c238e..cee455a5966b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1629,7 +1629,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.15.4 +opower==0.15.5 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5cedd3175c10..81337c500d41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1385,7 +1385,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.15.4 +opower==0.15.5 # homeassistant.components.oralb oralb-ble==0.17.6 From c745ee18eb597b7e3a3f97a26f41456035af6dcf Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 14 Sep 2025 17:51:47 +0200 Subject: [PATCH 82/85] Bump holidays to 0.80 (#152306) --- 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 5ea0d217f141..40c27762f008 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.79", "babel==2.15.0"] + "requirements": ["holidays==0.80", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 0e336632b2eb..8b917d5d8bd3 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.79"] + "requirements": ["holidays==0.80"] } diff --git a/requirements_all.txt b/requirements_all.txt index cee455a5966b..dd36311d0062 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1175,7 +1175,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.79 +holidays==0.80 # homeassistant.components.frontend home-assistant-frontend==20250903.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81337c500d41..6bd86f3cb528 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1024,7 +1024,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.79 +holidays==0.80 # homeassistant.components.frontend home-assistant-frontend==20250903.5 From be83416c72a60d71d69e09bfc30479f7bc7c74a2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 18 Sep 2025 20:35:21 +0200 Subject: [PATCH 83/85] Bump holidays to 0.81 (#152569) --- 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 40c27762f008..82e83275b6b6 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.80", "babel==2.15.0"] + "requirements": ["holidays==0.81", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 8b917d5d8bd3..c7a97ffb392d 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.80"] + "requirements": ["holidays==0.81"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd36311d0062..c29a48fb708b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1175,7 +1175,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.80 +holidays==0.81 # homeassistant.components.frontend home-assistant-frontend==20250903.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bd86f3cb528..979d206beab0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1024,7 +1024,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.80 +holidays==0.81 # homeassistant.components.frontend home-assistant-frontend==20250903.5 From 6dc78707792c3aee6a4a9608d9d12627b536346b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Sep 2025 20:07:56 +0000 Subject: [PATCH 84/85] Bump version to 2025.9.4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 217247dedfcc..608bf5d782c5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index ef4603faa818..fcb7a8a004b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.3" +version = "2025.9.4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 3f3aaa2815f3f4cdc511e5f4549d621437905c7f Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Wed, 3 Sep 2025 12:16:00 +0200 Subject: [PATCH 85/85] Bump asusrouter to 1.21.0 (#151607) --- homeassistant/components/asuswrt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 0fcc6f2d3d0c..6273c77ca783 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aioasuswrt", "asusrouter", "asyncssh"], - "requirements": ["aioasuswrt==1.4.0", "asusrouter==1.20.1"] + "requirements": ["aioasuswrt==1.4.0", "asusrouter==1.21.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c29a48fb708b..d3e897c62fb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -528,7 +528,7 @@ arris-tg2492lg==2.2.0 asmog==0.0.6 # homeassistant.components.asuswrt -asusrouter==1.20.1 +asusrouter==1.21.0 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 979d206beab0..0c89951f412f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,7 +492,7 @@ aranet4==2.5.1 arcam-fmj==1.8.2 # homeassistant.components.asuswrt -asusrouter==1.20.1 +asusrouter==1.21.0 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms