From 41ab7b346cf5d126c3ea7f9a911ea9b6092e3f76 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:08:58 +0200 Subject: [PATCH 01/96] Set timeout for remote calendar (#147024) --- .../components/remote_calendar/client.py | 12 ++++++++++++ .../components/remote_calendar/config_flow.py | 12 +++++++++--- .../components/remote_calendar/coordinator.py | 15 +++++++++++---- .../components/remote_calendar/strings.json | 6 +++++- .../remote_calendar/test_config_flow.py | 12 +++++++----- tests/components/remote_calendar/test_init.py | 7 ++++--- 6 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/remote_calendar/client.py diff --git a/homeassistant/components/remote_calendar/client.py b/homeassistant/components/remote_calendar/client.py new file mode 100644 index 00000000000000..f0f243ca386383 --- /dev/null +++ b/homeassistant/components/remote_calendar/client.py @@ -0,0 +1,12 @@ +"""Specifies the parameter for the httpx download.""" + +from httpx import AsyncClient, Response, Timeout + + +async def get_calendar(client: AsyncClient, url: str) -> Response: + """Make an HTTP GET request using Home Assistant's async HTTPX client with timeout.""" + return await client.get( + url, + follow_redirects=True, + timeout=Timeout(5, read=30, write=5, pool=5), + ) diff --git a/homeassistant/components/remote_calendar/config_flow.py b/homeassistant/components/remote_calendar/config_flow.py index 558a3d668aee2a..3f835b5d82b56a 100644 --- a/homeassistant/components/remote_calendar/config_flow.py +++ b/homeassistant/components/remote_calendar/config_flow.py @@ -4,13 +4,14 @@ import logging from typing import Any -from httpx import HTTPError, InvalidURL +from httpx import HTTPError, InvalidURL, TimeoutException import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL from homeassistant.helpers.httpx_client import get_async_client +from .client import get_calendar from .const import CONF_CALENDAR_NAME, DOMAIN from .ics import InvalidIcsException, parse_calendar @@ -49,7 +50,7 @@ async def async_step_user( self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) client = get_async_client(self.hass) try: - res = await client.get(user_input[CONF_URL], follow_redirects=True) + res = await get_calendar(client, user_input[CONF_URL]) if res.status_code == HTTPStatus.FORBIDDEN: errors["base"] = "forbidden" return self.async_show_form( @@ -58,9 +59,14 @@ async def async_step_user( errors=errors, ) res.raise_for_status() + except TimeoutException as err: + errors["base"] = "timeout_connect" + _LOGGER.debug( + "A timeout error occurred: %s", str(err) or type(err).__name__ + ) except (HTTPError, InvalidURL) as err: errors["base"] = "cannot_connect" - _LOGGER.debug("An error occurred: %s", err) + _LOGGER.debug("An error occurred: %s", str(err) or type(err).__name__) else: try: await parse_calendar(self.hass, res.text) diff --git a/homeassistant/components/remote_calendar/coordinator.py b/homeassistant/components/remote_calendar/coordinator.py index 1eead7682d3372..26876b532244a7 100644 --- a/homeassistant/components/remote_calendar/coordinator.py +++ b/homeassistant/components/remote_calendar/coordinator.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging -from httpx import HTTPError, InvalidURL +from httpx import HTTPError, InvalidURL, TimeoutException from ical.calendar import Calendar from homeassistant.config_entries import ConfigEntry @@ -12,6 +12,7 @@ from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .client import get_calendar from .const import DOMAIN from .ics import InvalidIcsException, parse_calendar @@ -36,7 +37,7 @@ def __init__( super().__init__( hass, _LOGGER, - name=DOMAIN, + name=f"{DOMAIN}_{config_entry.title}", update_interval=SCAN_INTERVAL, always_update=True, ) @@ -46,13 +47,19 @@ def __init__( async def _async_update_data(self) -> Calendar: """Update data from the url.""" try: - res = await self._client.get(self._url, follow_redirects=True) + res = await get_calendar(self._client, self._url) res.raise_for_status() + except TimeoutException as err: + _LOGGER.debug("%s: %s", self._url, str(err) or type(err).__name__) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="timeout", + ) from err except (HTTPError, InvalidURL) as err: + _LOGGER.debug("%s: %s", self._url, str(err) or type(err).__name__) raise UpdateFailed( translation_domain=DOMAIN, translation_key="unable_to_fetch", - translation_placeholders={"err": str(err)}, ) from err try: self.ics = res.text diff --git a/homeassistant/components/remote_calendar/strings.json b/homeassistant/components/remote_calendar/strings.json index ef7f20d4699abd..48ef6080bdb612 100644 --- a/homeassistant/components/remote_calendar/strings.json +++ b/homeassistant/components/remote_calendar/strings.json @@ -18,14 +18,18 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }, "error": { + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "forbidden": "The server understood the request but refuses to authorize it.", "invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details." } }, "exceptions": { + "timeout": { + "message": "The connection timed out. See the debug log for additional details." + }, "unable_to_fetch": { - "message": "Unable to fetch calendar data: {err}" + "message": "Unable to fetch calendar data. See the debug log for additional details." }, "unable_to_parse": { "message": "Unable to parse calendar data: {err}" diff --git a/tests/components/remote_calendar/test_config_flow.py b/tests/components/remote_calendar/test_config_flow.py index 9aff1594db32ea..9bea46ab27e647 100644 --- a/tests/components/remote_calendar/test_config_flow.py +++ b/tests/components/remote_calendar/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Remote Calendar config flow.""" -from httpx import ConnectError, Response, UnsupportedProtocol +from httpx import HTTPError, InvalidURL, Response, TimeoutException import pytest import respx @@ -75,10 +75,11 @@ async def test_form_import_webcal(hass: HomeAssistant, ics_content: str) -> None @pytest.mark.parametrize( - ("side_effect"), + ("side_effect", "base_error"), [ - ConnectError("Connection failed"), - UnsupportedProtocol("Unsupported protocol"), + (TimeoutException("Connection timed out"), "timeout_connect"), + (HTTPError("Connection failed"), "cannot_connect"), + (InvalidURL("Unsupported protocol"), "cannot_connect"), ], ) @respx.mock @@ -86,6 +87,7 @@ async def test_form_inavild_url( hass: HomeAssistant, side_effect: Exception, ics_content: str, + base_error: str, ) -> None: """Test we get the import form.""" result = await hass.config_entries.flow.async_init( @@ -102,7 +104,7 @@ async def test_form_inavild_url( }, ) assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": base_error} respx.get(CALENDER_URL).mock( return_value=Response( status_code=200, diff --git a/tests/components/remote_calendar/test_init.py b/tests/components/remote_calendar/test_init.py index f4ca500b2e17e3..d3e6b439805e95 100644 --- a/tests/components/remote_calendar/test_init.py +++ b/tests/components/remote_calendar/test_init.py @@ -1,6 +1,6 @@ """Tests for init platform of Remote Calendar.""" -from httpx import ConnectError, Response, UnsupportedProtocol +from httpx import HTTPError, InvalidURL, Response, TimeoutException import pytest import respx @@ -56,8 +56,9 @@ async def test_raise_for_status( @pytest.mark.parametrize( "side_effect", [ - ConnectError("Connection failed"), - UnsupportedProtocol("Unsupported protocol"), + TimeoutException("Connection timed out"), + HTTPError("Connection failed"), + InvalidURL("Unsupported protocol"), ValueError("Invalid response"), ], ) From 9a4959560e0330d2523f074679a16dfb2ab55d3b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 3 Jul 2025 08:51:38 +0200 Subject: [PATCH 02/96] Fix missing port in samsungtv (#147962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- .../components/samsungtv/config_flow.py | 25 +++++++++++-------- .../components/samsungtv/test_config_flow.py | 17 +++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index dbde1ee1ef3584..e2b9f8631d838a 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -124,6 +124,7 @@ def __init__(self) -> None: self._model: str | None = None self._connect_result: str | None = None self._method: str | None = None + self._port: int | None = None self._name: str | None = None self._title: str = "" self._id: int | None = None @@ -199,33 +200,37 @@ def _async_update_and_abort_for_matching_unique_id(self) -> None: async def _async_create_bridge(self) -> None: """Create the bridge.""" - result, method, _info = await self._async_get_device_info_and_method() + result = await self._async_load_device_info() if result not in SUCCESSFUL_RESULTS: LOGGER.debug("No working config found for %s", self._host) raise AbortFlow(result) - assert method is not None - self._bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host) + assert self._method is not None + self._bridge = SamsungTVBridge.get_bridge( + self.hass, self._method, self._host, self._port + ) - async def _async_get_device_info_and_method( + async def _async_load_device_info( self, - ) -> tuple[str, str | None, dict[str, Any] | None]: + ) -> str: """Get device info and method only once.""" if self._connect_result is None: - result, _, method, info = await async_get_device_info(self.hass, self._host) + result, port, method, info = await async_get_device_info( + self.hass, self._host + ) self._connect_result = result self._method = method + self._port = port self._device_info = info if not method: LOGGER.debug("Host:%s did not return device info", self._host) - return result, None, None - return self._connect_result, self._method, self._device_info + return self._connect_result async def _async_get_and_check_device_info(self) -> bool: """Try to get the device info.""" - result, _method, info = await self._async_get_device_info_and_method() + result = await self._async_load_device_info() if result not in SUCCESSFUL_RESULTS: raise AbortFlow(result) - if not info: + if not (info := self._device_info): return False dev_info = info.get("device", {}) assert dev_info is not None diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index d63e5a7ae2aeb7..dd6b21ab5e5469 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -161,6 +161,7 @@ async def test_user_legacy(hass: HomeAssistant) -> None: assert result["data"][CONF_METHOD] == METHOD_LEGACY assert result["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result["data"][CONF_MODEL] is None + assert result["data"][CONF_PORT] == 55000 assert result["result"].unique_id is None @@ -195,6 +196,7 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: assert result3["data"][CONF_METHOD] == METHOD_LEGACY assert result3["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result3["data"][CONF_MODEL] is None + assert result3["data"][CONF_PORT] == 55000 assert result3["result"].unique_id is None @@ -224,6 +226,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None: assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -272,6 +275,7 @@ async def test_user_encrypted_websocket( assert result4["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result4["data"][CONF_MANUFACTURER] == "Samsung" assert result4["data"][CONF_MODEL] == "UE48JU6400" + assert result4["data"][CONF_PORT] == 8000 assert result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] is None assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" @@ -402,6 +406,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: assert result["data"][CONF_HOST] == "10.20.43.21" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -464,6 +469,7 @@ async def test_ssdp(hass: HomeAssistant) -> None: assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["data"][CONF_PORT] == 55000 assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @@ -522,6 +528,7 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["data"][CONF_PORT] == 55000 assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @@ -557,6 +564,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["data"][CONF_PORT] == 55000 assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @@ -599,6 +607,7 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert ( result["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] == "http://10.10.12.34:7676/smp_15_" @@ -630,6 +639,7 @@ async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_loc assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert ( result["data"][CONF_SSDP_MAIN_TV_AGENT_LOCATION] == "http://10.10.12.34:7676/smp_2_" @@ -681,6 +691,7 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l assert result4["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result4["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result4["data"][CONF_MODEL] == "UE48JU6400" + assert result4["data"][CONF_PORT] == 8000 assert ( result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] == "http://10.10.12.34:7676/smp_15_" @@ -887,6 +898,7 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "UE48JU6400" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @@ -919,6 +931,7 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: assert result["data"][CONF_MAC] == "aa:ee:tt:hh:ee:rr" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "UE43LS003" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1020,6 +1033,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1129,6 +1143,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" + assert result["data"][CONF_PORT] == 8002 remote_websocket.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() @@ -1180,6 +1195,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" assert result["data"][CONF_MAC] == "gg:ee:tt:mm:aa:cc" + assert result["data"][CONF_PORT] == 8002 remote_websocket.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() @@ -2091,6 +2107,7 @@ async def test_ssdp_update_mac(hass: HomeAssistant) -> None: assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert entry.data[CONF_MODEL] == "fake_model" assert entry.data[CONF_MAC] is None + assert entry.data[CONF_PORT] == 8002 assert entry.unique_id == "123" device_info = deepcopy(MOCK_DEVICE_INFO) From 0f32b6331d8928a5ed814eb39955e0181bcaebe9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:19:32 -0400 Subject: [PATCH 03/96] Bump ZHA to 0.0.62 (#147966) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../zha/snapshots/test_diagnostics.ambr | 51 ------------------- 4 files changed, 3 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4fb5f57320fd02..2cbc962a305f96 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.61"], + "requirements": ["zha==0.0.62"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 04b3f0eaa2fe77..4a4454e9dab1ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3190,7 +3190,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.61 +zha==0.0.62 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea925412def329..c40ea6e17fcfa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2634,7 +2634,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.61 +zha==0.0.62 # homeassistant.components.zwave_js zwave-js-server-python==0.65.0 diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index 44fb913489d080..35eb320893f3f0 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -168,7 +168,6 @@ dict({ 'id': '0x0010', 'name': 'cie_addr', - 'unsupported': False, 'value': list([ 50, 79, @@ -181,68 +180,18 @@ ]), 'zcl_type': 'EUI64', }), - dict({ - 'id': '0x0013', - 'name': 'current_zone_sensitivity_level', - 'unsupported': False, - 'value': None, - 'zcl_type': 'uint8', - }), dict({ 'id': '0x0012', 'name': 'num_zone_sensitivity_levels_supported', 'unsupported': True, - 'value': None, - 'zcl_type': 'uint8', - }), - dict({ - 'id': '0x0011', - 'name': 'zone_id', - 'unsupported': False, - 'value': None, 'zcl_type': 'uint8', }), - dict({ - 'id': '0x0000', - 'name': 'zone_state', - 'unsupported': False, - 'value': None, - 'zcl_type': 'enum8', - }), - dict({ - 'id': '0x0002', - 'name': 'zone_status', - 'unsupported': False, - 'value': None, - 'zcl_type': 'map16', - }), - dict({ - 'id': '0x0001', - 'name': 'zone_type', - 'unsupported': False, - 'value': None, - 'zcl_type': 'uint16', - }), ]), 'cluster_id': '0x0500', 'endpoint_attribute': 'ias_zone', }), dict({ 'attributes': list([ - dict({ - 'id': '0xfffd', - 'name': 'cluster_revision', - 'unsupported': False, - 'value': None, - 'zcl_type': 'uint16', - }), - dict({ - 'id': '0xfffe', - 'name': 'reporting_status', - 'unsupported': False, - 'value': None, - 'zcl_type': 'enum8', - }), ]), 'cluster_id': '0x0501', 'endpoint_attribute': 'ias_ace', From a2ffe32b025e3ecf2150c48f409d609f9bc22ecd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Jul 2025 23:10:21 +0200 Subject: [PATCH 04/96] Bump aiounifi to v84 (#147987) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index dd255c57c131ef..d13b180d62d016 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -7,7 +7,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["aiounifi"], - "requirements": ["aiounifi==83"], + "requirements": ["aiounifi==84"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 4a4454e9dab1ae..2fece9385aa751 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -414,7 +414,7 @@ aiotedee==0.2.25 aiotractive==0.6.0 # homeassistant.components.unifi -aiounifi==83 +aiounifi==84 # homeassistant.components.usb aiousbwatcher==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c40ea6e17fcfa3..6118a3ede0baac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -396,7 +396,7 @@ aiotedee==0.2.25 aiotractive==0.6.0 # homeassistant.components.unifi -aiounifi==83 +aiounifi==84 # homeassistant.components.usb aiousbwatcher==1.1.1 From c23bfb1b39d083ce2fae27f0545d7061b8608736 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 3 Jul 2025 08:50:41 +0200 Subject: [PATCH 05/96] Fix state being incorrectly reported in some situations on Music Assistant players (#147997) --- homeassistant/components/music_assistant/manifest.json | 2 +- homeassistant/components/music_assistant/media_player.py | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/music_assistant/manifest.json b/homeassistant/components/music_assistant/manifest.json index e29491e2b219a1..4b28a1029a4bd9 100644 --- a/homeassistant/components/music_assistant/manifest.json +++ b/homeassistant/components/music_assistant/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/music_assistant", "iot_class": "local_push", "loggers": ["music_assistant"], - "requirements": ["music-assistant-client==1.2.3"], + "requirements": ["music-assistant-client==1.2.4"], "zeroconf": ["_mass._tcp.local."] } diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index b748aad241c290..3a210856391084 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -248,8 +248,6 @@ async def async_on_update(self) -> None: player = self.player active_queue = self.active_queue # update generic attributes - if player.powered and active_queue is not None: - self._attr_state = MediaPlayerState(active_queue.state.value) if player.powered and player.playback_state is not None: self._attr_state = MediaPlayerState(player.playback_state.value) else: diff --git a/requirements_all.txt b/requirements_all.txt index 2fece9385aa751..e67b8687e5debc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1467,7 +1467,7 @@ mozart-api==4.1.1.116.4 mullvad-api==1.0.0 # homeassistant.components.music_assistant -music-assistant-client==1.2.3 +music-assistant-client==1.2.4 # homeassistant.components.tts mutagen==1.47.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6118a3ede0baac..ca114781c23288 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1259,7 +1259,7 @@ mozart-api==4.1.1.116.4 mullvad-api==1.0.0 # homeassistant.components.music_assistant -music-assistant-client==1.2.3 +music-assistant-client==1.2.4 # homeassistant.components.tts mutagen==1.47.0 From f806e6ba49687105249baed3a242d74adbd29eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 3 Jul 2025 13:24:51 +0100 Subject: [PATCH 06/96] Bump hass-nabucasa from 0.104.0 to 0.105.0 (#148040) --- 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 70cf6a2c0720f4..0d44d57ac5ea7f 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.104.0"], + "requirements": ["hass-nabucasa==0.105.0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f0fa428fbb3c5a..e666f06c5f28b2 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==3.49.0 -hass-nabucasa==0.104.0 +hass-nabucasa==0.105.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250702.0 diff --git a/pyproject.toml b/pyproject.toml index c34a268e347bfa..77d788a1dd05c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.104.0", + "hass-nabucasa==0.105.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", diff --git a/requirements.txt b/requirements.txt index 1791d12268bee6..011d76e66b50c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.104.0 +hass-nabucasa==0.105.0 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e67b8687e5debc..dd663763a54da6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,7 +1127,7 @@ habiticalib==0.4.0 habluetooth==3.49.0 # homeassistant.components.cloud -hass-nabucasa==0.104.0 +hass-nabucasa==0.105.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca114781c23288..f624e1d552b337 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ habiticalib==0.4.0 habluetooth==3.49.0 # homeassistant.components.cloud -hass-nabucasa==0.104.0 +hass-nabucasa==0.105.0 # homeassistant.components.assist_satellite # homeassistant.components.conversation From 898ef43750b1c7bd2e304f01f635e2603b1d61ee Mon Sep 17 00:00:00 2001 From: hanwg Date: Fri, 4 Jul 2025 22:32:46 +0800 Subject: [PATCH 07/96] Fix Telegram bots using plain text parser failing to load on restart (#148050) --- homeassistant/components/telegram_bot/bot.py | 6 +++--- homeassistant/components/telegram_bot/config_flow.py | 2 -- homeassistant/components/telegram_bot/services.yaml | 5 +++++ tests/components/telegram_bot/test_config_flow.py | 2 +- tests/components/telegram_bot/test_telegram_bot.py | 5 ++++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/telegram_bot/bot.py b/homeassistant/components/telegram_bot/bot.py index a3feb120460d0e..c57648c9551f55 100644 --- a/homeassistant/components/telegram_bot/bot.py +++ b/homeassistant/components/telegram_bot/bot.py @@ -374,9 +374,7 @@ def _make_row_inline_keyboard(row_keyboard: Any) -> list[InlineKeyboardButton]: } if data is not None: if ATTR_PARSER in data: - params[ATTR_PARSER] = self._parsers.get( - data[ATTR_PARSER], self.parse_mode - ) + params[ATTR_PARSER] = data[ATTR_PARSER] if ATTR_TIMEOUT in data: params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT] if ATTR_DISABLE_NOTIF in data: @@ -408,6 +406,8 @@ def _make_row_inline_keyboard(row_keyboard: Any) -> list[InlineKeyboardButton]: params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup( [_make_row_inline_keyboard(row) for row in keys] ) + if params[ATTR_PARSER] == PARSER_PLAIN_TEXT: + params[ATTR_PARSER] = None return params async def _send_msg( diff --git a/homeassistant/components/telegram_bot/config_flow.py b/homeassistant/components/telegram_bot/config_flow.py index 41f26ccd48d341..8d3d9b0cd7b3ca 100644 --- a/homeassistant/components/telegram_bot/config_flow.py +++ b/homeassistant/components/telegram_bot/config_flow.py @@ -159,8 +159,6 @@ async def async_step_init( """Manage the options.""" if user_input is not None: - if user_input[ATTR_PARSER] == PARSER_PLAIN_TEXT: - user_input[ATTR_PARSER] = None return self.async_create_entry(data=user_input) return self.async_show_form( diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index d5fc0e134d5c26..b1d94d381ac50b 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -109,6 +109,7 @@ send_photo: - "markdown" - "markdownv2" - "plain_text" + translation_key: "parse_mode" disable_notification: selector: boolean: @@ -261,6 +262,7 @@ send_animation: - "markdown" - "markdownv2" - "plain_text" + translation_key: "parse_mode" disable_notification: selector: boolean: @@ -341,6 +343,7 @@ send_video: - "markdown" - "markdownv2" - "plain_text" + translation_key: "parse_mode" disable_notification: selector: boolean: @@ -493,6 +496,7 @@ send_document: - "markdown" - "markdownv2" - "plain_text" + translation_key: "parse_mode" disable_notification: selector: boolean: @@ -670,6 +674,7 @@ edit_message: - "markdown" - "markdownv2" - "plain_text" + translation_key: "parse_mode" disable_web_page_preview: selector: boolean: diff --git a/tests/components/telegram_bot/test_config_flow.py b/tests/components/telegram_bot/test_config_flow.py index 2586761b58435e..9a076016a32fd6 100644 --- a/tests/components/telegram_bot/test_config_flow.py +++ b/tests/components/telegram_bot/test_config_flow.py @@ -63,7 +63,7 @@ async def test_options_flow( await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][ATTR_PARSER] is None + assert result["data"][ATTR_PARSER] == PARSER_PLAIN_TEXT async def test_reconfigure_flow_broadcast( diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index 6590bbed1cf6d6..73dd9e277634d0 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -50,6 +50,7 @@ ATTR_VERIFY_SSL, CONF_CONFIG_ENTRY_ID, DOMAIN, + PARSER_PLAIN_TEXT, PLATFORM_BROADCAST, SECTION_ADVANCED_SETTINGS, SERVICE_ANSWER_CALLBACK_QUERY, @@ -183,6 +184,7 @@ async def test_send_message( ( { ATTR_MESSAGE: "test_message", + ATTR_PARSER: PARSER_PLAIN_TEXT, ATTR_KEYBOARD_INLINE: "command1:/cmd1,/cmd2,mock_link:https://mock_link", }, InlineKeyboardMarkup( @@ -199,6 +201,7 @@ async def test_send_message( ( { ATTR_MESSAGE: "test_message", + ATTR_PARSER: PARSER_PLAIN_TEXT, ATTR_KEYBOARD_INLINE: [ [["command1", "/cmd1"]], [["mock_link", "https://mock_link"]], @@ -250,7 +253,7 @@ async def test_send_message_with_inline_keyboard( mock_send_message.assert_called_once_with( 12345678, "test_message", - parse_mode=ParseMode.MARKDOWN, + parse_mode=None, disable_web_page_preview=None, disable_notification=False, reply_to_message_id=None, From f42e7d982f78d2c4ee99fa813d14d600f1a4cf40 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:04:13 +0200 Subject: [PATCH 08/96] Bump pyenphase to 2.2.0 (#148070) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 5f74da954a06ef..8387ecc9c9f6ac 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["pyenphase"], "quality_scale": "platinum", - "requirements": ["pyenphase==2.1.0"], + "requirements": ["pyenphase==2.2.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index dd663763a54da6..fb548289de5f18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1962,7 +1962,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==2.1.0 +pyenphase==2.2.0 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f624e1d552b337..e3b58a5dfe8e76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1637,7 +1637,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==2.1.0 +pyenphase==2.2.0 # homeassistant.components.everlights pyeverlights==0.1.0 From eb58c10e5ee87d0d0098c868de2acba3a4284ac0 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Thu, 3 Jul 2025 22:06:38 +0200 Subject: [PATCH 09/96] Cancel enphase mac verification on unload. (#148072) --- homeassistant/components/enphase_envoy/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index eee6cb85e6d1f5..f43d89aa098450 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -63,6 +63,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> coordinator = entry.runtime_data coordinator.async_cancel_token_refresh() coordinator.async_cancel_firmware_refresh() + coordinator.async_cancel_mac_verification() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) From 342b4c344249340a9f3f44538d164325e6cb6dea Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Jul 2025 17:33:16 +0300 Subject: [PATCH 10/96] Bump aioamazondevices to 3.2.3 (#148082) --- homeassistant/components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 7c23edd92ce597..70281390436805 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.2.2"] + "requirements": ["aioamazondevices==3.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index fb548289de5f18..cae697cdfe96be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.2.2 +aioamazondevices==3.2.3 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3b58a5dfe8e76..de092158c7e71f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.2.2 +aioamazondevices==3.2.3 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From c646658643a8d2e056d229498b0e70385f90a11d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Jul 2025 16:25:28 +0200 Subject: [PATCH 11/96] Update frontend to 20250702.1 (#148131) --- 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 bfd868a5334071..748d8f0c6f0052 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==20250702.0"] + "requirements": ["home-assistant-frontend==20250702.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e666f06c5f28b2..824c3d945fe93e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.49.0 hass-nabucasa==0.105.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250702.0 +home-assistant-frontend==20250702.1 home-assistant-intents==2025.6.23 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index cae697cdfe96be..04ed2500e01f47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1168,7 +1168,7 @@ hole==0.8.0 holidays==0.75 # homeassistant.components.frontend -home-assistant-frontend==20250702.0 +home-assistant-frontend==20250702.1 # homeassistant.components.conversation home-assistant-intents==2025.6.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de092158c7e71f..fede23373533c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,7 +1017,7 @@ hole==0.8.0 holidays==0.75 # homeassistant.components.frontend -home-assistant-frontend==20250702.0 +home-assistant-frontend==20250702.1 # homeassistant.components.conversation home-assistant-intents==2025.6.23 From 3ffec2a655f74ac805d32f78164a14cad25678b3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:22:38 +0200 Subject: [PATCH 12/96] [ci] Fix typing issue with aiohttp and aiosignal (#148141) --- .github/workflows/ci.yaml | 2 +- homeassistant/components/http/ban.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 19cc8bd3af7147..c5491df002161f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 3 + CACHE_VERSION: 4 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.7" diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 71f3d54bef6abf..7e55191639b997 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -64,7 +64,7 @@ async def ban_startup(app: Application) -> None: """Initialize bans when app starts up.""" await app[KEY_BAN_MANAGER].async_load() - app.on_startup.append(ban_startup) + app.on_startup.append(ban_startup) # type: ignore[arg-type] @middleware From 4e163c4591529f09b90f1fb4d299a00663fac563 Mon Sep 17 00:00:00 2001 From: Michael Freeman Date: Fri, 4 Jul 2025 11:46:59 -0400 Subject: [PATCH 13/96] Bump venstarcolortouch to 0.21 (#148152) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index f3045fe49e8a70..5991dc8fe5139c 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/venstar", "iot_class": "local_polling", "loggers": ["venstarcolortouch"], - "requirements": ["venstarcolortouch==0.19"] + "requirements": ["venstarcolortouch==0.21"] } diff --git a/requirements_all.txt b/requirements_all.txt index 04ed2500e01f47..fcd97e305fc379 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3041,7 +3041,7 @@ vehicle==2.2.2 velbus-aio==2025.5.0 # homeassistant.components.venstar -venstarcolortouch==0.19 +venstarcolortouch==0.21 # homeassistant.components.vilfo vilfo-api-client==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fede23373533c6..a234822689c74c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2509,7 +2509,7 @@ vehicle==2.2.2 velbus-aio==2025.5.0 # homeassistant.components.venstar -venstarcolortouch==0.19 +venstarcolortouch==0.21 # homeassistant.components.vilfo vilfo-api-client==0.5.0 From a274961593b41bd2a2aa950427a437da55914b4e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Jul 2025 19:22:41 +0000 Subject: [PATCH 14/96] Bump version to 2025.7.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 f1a9f7f79c2106..258a9c9a48f2f2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 7 -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 77d788a1dd05c6..b4478a69d53f77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.7.0" +version = "2025.7.1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 2b7992e849217ef85d272542c951d837d24d0c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Sat, 5 Jul 2025 11:36:45 +0200 Subject: [PATCH 15/96] Squeezebox: Fix track selection in media browser (#147185) --- homeassistant/components/squeezebox/browse_media.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 03df289a2fdee2..f09c2e3f2a7839 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -311,8 +311,7 @@ async def build_item_response( title=item["title"], media_content_type=item_type, media_class=CONTENT_TYPE_MEDIA_CLASS[item_type]["item"], - can_expand=CONTENT_TYPE_MEDIA_CLASS[item_type]["children"] - is not None, + can_expand=bool(CONTENT_TYPE_MEDIA_CLASS[item_type]["children"]), can_play=True, ) From 9077965214402a69d4a1fb5d7c66dbb2562e5738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Sat, 5 Jul 2025 11:31:30 +0200 Subject: [PATCH 16/96] Squeezebox: Fix tracks not having thumbnails (#147187) --- homeassistant/components/squeezebox/browse_media.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index f09c2e3f2a7839..bab4f90c6d13a6 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -221,12 +221,16 @@ def _get_item_thumbnail( ) -> str | None: """Construct path to thumbnail image.""" item_thumbnail: str | None = None - if artwork_track_id := item.get("artwork_track_id"): + track_id = item.get("artwork_track_id") or ( + item.get("id") if item_type == "track" else None + ) + + if track_id: if internal_request: - item_thumbnail = player.generate_image_url_from_track_id(artwork_track_id) + item_thumbnail = player.generate_image_url_from_track_id(track_id) elif item_type is not None: item_thumbnail = entity.get_browse_image_url( - item_type, item["id"], artwork_track_id + item_type, item["id"], track_id ) elif search_type in ["apps", "radios"]: From c965da655924dfc987df5f2091bb4f1fa97662b7 Mon Sep 17 00:00:00 2001 From: TimL Date: Sat, 5 Jul 2025 06:35:44 +1000 Subject: [PATCH 17/96] Bump pysmlight to v0.2.7 (#148101) Co-authored-by: Franck Nijhof --- homeassistant/components/smlight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smlight/manifest.json b/homeassistant/components/smlight/manifest.json index 9a37cc554c7bd1..9340573f6ceba1 100644 --- a/homeassistant/components/smlight/manifest.json +++ b/homeassistant/components/smlight/manifest.json @@ -12,7 +12,7 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "silver", - "requirements": ["pysmlight==0.2.6"], + "requirements": ["pysmlight==0.2.7"], "zeroconf": [ { "type": "_slzb-06._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index fcd97e305fc379..3a612431369682 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2360,7 +2360,7 @@ pysmhi==1.0.2 pysml==0.1.5 # homeassistant.components.smlight -pysmlight==0.2.6 +pysmlight==0.2.7 # homeassistant.components.snmp pysnmp==6.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a234822689c74c..e284b9dbb1829b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1963,7 +1963,7 @@ pysmhi==1.0.2 pysml==0.1.5 # homeassistant.components.smlight -pysmlight==0.2.6 +pysmlight==0.2.7 # homeassistant.components.snmp pysnmp==6.2.6 From 9650727515df49c51a193ffd71a255d7db2bc4ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 08:32:58 -0500 Subject: [PATCH 18/96] Fix REST sensor charset handling to respect Content-Type header (#148223) --- homeassistant/components/rest/data.py | 9 ++- tests/components/rest/test_sensor.py | 88 +++++++++++++++++++++++++++ tests/test_util/aiohttp.py | 21 ++++++- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 731d1ffe9c3371..3ce70f00c0d348 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -140,7 +140,14 @@ async def async_update(self, log_errors: bool = True) -> None: self._method, self._resource, **request_kwargs ) as response: # Read the response - self.data = await response.text(encoding=self._encoding) + # Only use configured encoding if no charset in Content-Type header + # If charset is present in Content-Type, let aiohttp use it + if response.charset: + # Let aiohttp use the charset from Content-Type header + self.data = await response.text() + else: + # Use configured encoding as fallback + self.data = await response.text(encoding=self._encoding) self.headers = response.headers except TimeoutError as ex: diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index c688ff1b314cb6..cbc77e9d53fb2f 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -162,6 +162,94 @@ async def test_setup_encoding( assert hass.states.get("sensor.mysensor").state == "tack själv" +async def test_setup_auto_encoding_from_content_type( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with encoding auto-detected from Content-Type header.""" + # Test with ISO-8859-1 charset in Content-Type header + aioclient_mock.get( + "http://localhost", + status=HTTPStatus.OK, + content="Björk Guðmundsdóttir".encode("iso-8859-1"), + headers={"Content-Type": "text/plain; charset=iso-8859-1"}, + ) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "name": "mysensor", + # encoding defaults to UTF-8, but should be ignored when charset present + "platform": DOMAIN, + "resource": "http://localhost", + "method": "GET", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1 + assert hass.states.get("sensor.mysensor").state == "Björk Guðmundsdóttir" + + +async def test_setup_encoding_fallback_no_charset( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that configured encoding is used when no charset in Content-Type.""" + # No charset in Content-Type header + aioclient_mock.get( + "http://localhost", + status=HTTPStatus.OK, + content="Björk Guðmundsdóttir".encode("iso-8859-1"), + headers={"Content-Type": "text/plain"}, # No charset! + ) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "name": "mysensor", + "encoding": "iso-8859-1", # This will be used as fallback + "platform": DOMAIN, + "resource": "http://localhost", + "method": "GET", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1 + assert hass.states.get("sensor.mysensor").state == "Björk Guðmundsdóttir" + + +async def test_setup_charset_overrides_encoding_config( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that charset in Content-Type overrides configured encoding.""" + # Server sends UTF-8 with correct charset header + aioclient_mock.get( + "http://localhost", + status=HTTPStatus.OK, + content="Björk Guðmundsdóttir".encode(), + headers={"Content-Type": "text/plain; charset=utf-8"}, + ) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "name": "mysensor", + "encoding": "iso-8859-1", # Config says ISO-8859-1, but charset=utf-8 should win + "platform": DOMAIN, + "resource": "http://localhost", + "method": "GET", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1 + # This should work because charset=utf-8 overrides the iso-8859-1 config + assert hass.states.get("sensor.mysensor").state == "Björk Guðmundsdóttir" + + @pytest.mark.parametrize( ("ssl_cipher_list", "ssl_cipher_list_expected"), [ diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index eea3f4e88b485b..a3c714e1f62948 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -191,7 +191,6 @@ def __init__( if response is None: response = b"" - self.charset = "utf-8" self.method = method self._url = url self.status = status @@ -261,16 +260,32 @@ def content(self): """Return content.""" return mock_stream(self.response) + @property + def charset(self): + """Return charset from Content-Type header.""" + if (content_type := self._headers.get("content-type")) is None: + return None + content_type = content_type.lower() + if "charset=" in content_type: + return content_type.split("charset=")[1].split(";")[0].strip() + return None + async def read(self): """Return mock response.""" return self.response - async def text(self, encoding="utf-8", errors="strict"): + async def text(self, encoding=None, errors="strict") -> str: """Return mock response as a string.""" + # Match real aiohttp behavior: encoding=None means auto-detect + if encoding is None: + encoding = self.charset or "utf-8" return self.response.decode(encoding, errors=errors) - async def json(self, encoding="utf-8", content_type=None, loads=json_loads): + async def json(self, encoding=None, content_type=None, loads=json_loads) -> Any: """Return mock response as a json.""" + # Match real aiohttp behavior: encoding=None means auto-detect + if encoding is None: + encoding = self.charset or "utf-8" return loads(self.response.decode(encoding)) def release(self): From 74f9549431fb8ae906f1074a51affba15af07b45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Jul 2025 12:40:19 -0500 Subject: [PATCH 19/96] Fix UTF-8 encoding for REST basic authentication (#148225) --- homeassistant/components/rest/data.py | 2 +- tests/components/rest/test_binary_sensor.py | 33 +++++++++++++++++++++ tests/test_util/aiohttp.py | 3 ++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 3ce70f00c0d348..7f346277314678 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -49,7 +49,7 @@ def __init__( # Convert auth tuple to aiohttp.BasicAuth if needed if isinstance(auth, tuple) and len(auth) == 2: self._auth: aiohttp.BasicAuth | aiohttp.DigestAuthMiddleware | None = ( - aiohttp.BasicAuth(auth[0], auth[1]) + aiohttp.BasicAuth(auth[0], auth[1], encoding="utf-8") ) else: self._auth = auth diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 315f8113309e98..af7503a70076d7 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -667,3 +667,36 @@ async def test_availability_blocks_value_template( await hass.async_block_till_done() assert error in caplog.text + + +async def test_setup_get_basic_auth_utf8( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with basic auth using UTF-8 characters including Unicode char \u2018.""" + # Use a password with the Unicode character \u2018 (left single quotation mark) + aioclient_mock.get("http://localhost", status=HTTPStatus.OK, json={"key": "on"}) + assert await async_setup_component( + hass, + BINARY_SENSOR_DOMAIN, + { + BINARY_SENSOR_DOMAIN: { + "platform": DOMAIN, + "resource": "http://localhost", + "method": "GET", + "value_template": "{{ value_json.key }}", + "name": "foo", + "verify_ssl": "true", + "timeout": 30, + "authentication": "basic", + "username": "test_user", + "password": "test\u2018password", # Password with Unicode char + "headers": {"Accept": CONTENT_TYPE_JSON}, + } + }, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all(BINARY_SENSOR_DOMAIN)) == 1 + + state = hass.states.get("binary_sensor.foo") + assert state.state == STATE_ON diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index a3c714e1f62948..c3a8be77b77085 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -156,6 +156,9 @@ async def match_request( for response in self._mocks: if response.match_request(method, url, params): + # If auth is provided, try to encode it to trigger any encoding errors + if auth is not None: + auth.encode() self.mock_calls.append((method, url, data, headers)) if response.side_effect: response = await response.side_effect(method, url, data) From f4ca56052b575e161fef85cfd5dc9fd016b49d3a Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 5 Jul 2025 23:34:23 +0200 Subject: [PATCH 20/96] Bump pylamarzocco to 2.0.10 (#148233) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 7fdafc4dda17f1..10cb23146ae52e 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.9"] + "requirements": ["pylamarzocco==2.0.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3a612431369682..bac56ca7d70ba7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2100,7 +2100,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.9 +pylamarzocco==2.0.10 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e284b9dbb1829b..107b8d2170355c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1745,7 +1745,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.9 +pylamarzocco==2.0.10 # homeassistant.components.lastfm pylast==5.1.0 From b4d789f8e2df2515c7f02cdd9349314d972638bc Mon Sep 17 00:00:00 2001 From: Mark Adkins Date: Mon, 7 Jul 2025 09:18:15 -0400 Subject: [PATCH 21/96] Bump sharkiq to 1.1.1 (#148244) --- homeassistant/components/sharkiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sharkiq/manifest.json b/homeassistant/components/sharkiq/manifest.json index 9f9009693e53ff..c29fc582462c43 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.0"] + "requirements": ["sharkiq==1.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index bac56ca7d70ba7..3bc9d412cc54ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2756,7 +2756,7 @@ sentry-sdk==1.45.1 sfrbox-api==0.0.11 # homeassistant.components.sharkiq -sharkiq==1.1.0 +sharkiq==1.1.1 # homeassistant.components.aquostv sharp_aquos_rc==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 107b8d2170355c..5c9ae5c6235899 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2278,7 +2278,7 @@ sentry-sdk==1.45.1 sfrbox-api==0.0.11 # homeassistant.components.sharkiq -sharkiq==1.1.0 +sharkiq==1.1.1 # homeassistant.components.simplefin simplefin4py==0.0.18 From 59bcf1167a14b5e4c1d98dbbc7433aea948792c6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 6 Jul 2025 12:38:57 +0200 Subject: [PATCH 22/96] bump motionblinds to 0.6.29 (#148265) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index a82da20396ffd4..eca520d8946f8b 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.28"] + "requirements": ["motionblinds==0.6.29"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3bc9d412cc54ed..c886f94a68a61e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1452,7 +1452,7 @@ monzopy==1.4.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.28 +motionblinds==0.6.29 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c9ae5c6235899..51510f46a3385a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1244,7 +1244,7 @@ monzopy==1.4.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.28 +motionblinds==0.6.29 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 From 5c3b279f958349200f9265ebaab773ad5f160600 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 7 Jul 2025 12:37:39 +0300 Subject: [PATCH 23/96] Bump aiowebostv to 0.7.4 (#148273) --- .../components/webostv/config_flow.py | 5 +++- .../components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/webostv/test_config_flow.py | 30 ++++++++++++++++++- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 80c8fb7f8f27f2..2af38cb3d173f5 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -98,7 +98,10 @@ async def async_step_pairing( data = {CONF_HOST: self._host, CONF_CLIENT_SECRET: client.client_key} if not self._name: - self._name = f"{DEFAULT_NAME} {client.tv_info.system['modelName']}" + if model_name := client.tv_info.system.get("modelName"): + self._name = f"{DEFAULT_NAME} {model_name}" + else: + self._name = DEFAULT_NAME return self.async_create_entry(title=self._name, data=data) return self.async_show_form(step_id="pairing", errors=errors) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 8ac470ae922f23..c3c3e9a564f04e 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/webostv", "iot_class": "local_push", "loggers": ["aiowebostv"], - "requirements": ["aiowebostv==0.7.3"], + "requirements": ["aiowebostv==0.7.4"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/requirements_all.txt b/requirements_all.txt index c886f94a68a61e..7458c0f94e6603 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -435,7 +435,7 @@ aiowatttime==0.1.1 aiowebdav2==0.4.6 # homeassistant.components.webostv -aiowebostv==0.7.3 +aiowebostv==0.7.4 # homeassistant.components.withings aiowithings==3.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51510f46a3385a..13a04de90640a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -417,7 +417,7 @@ aiowatttime==0.1.1 aiowebdav2==0.4.6 # homeassistant.components.webostv -aiowebostv==0.7.3 +aiowebostv==0.7.4 # homeassistant.components.withings aiowithings==3.1.6 diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 564ff9afa9bc1c..2445140aff4885 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -4,7 +4,12 @@ import pytest from homeassistant import config_entries -from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN, LIVE_TV_APP_ID +from homeassistant.components.webostv.const import ( + CONF_SOURCES, + DEFAULT_NAME, + DOMAIN, + LIVE_TV_APP_ID, +) from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_SOURCE from homeassistant.core import HomeAssistant @@ -63,6 +68,29 @@ async def test_form(hass: HomeAssistant, client) -> None: assert config_entry.unique_id == FAKE_UUID +async def test_form_no_model_name(hass: HomeAssistant, client) -> None: + """Test successful user flow without model name.""" + client.tv_info.system = {} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_USER}, + data=MOCK_USER_CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "pairing" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + config_entry = result["result"] + assert config_entry.unique_id == FAKE_UUID + + @pytest.mark.parametrize( ("apps", "inputs"), [ From 3d3f2527cb5d96b919e21a8db458d9428bdc5bae Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 7 Jul 2025 11:43:39 +0200 Subject: [PATCH 24/96] Bump `gios` to version 6.1.0 (#148274) --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gios/__init__.py | 33 +++++-- tests/components/gios/fixtures/indexes.json | 63 +++++++----- tests/components/gios/fixtures/sensors.json | 56 +++++------ tests/components/gios/fixtures/station.json | 98 ++++++++----------- .../gios/snapshots/test_diagnostics.ambr | 2 + 8 files changed, 134 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 8deb2eee414254..ba87890de03b72 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["dacite", "gios"], - "requirements": ["gios==6.0.0"] + "requirements": ["gios==6.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7458c0f94e6603..8c4850e31627a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1020,7 +1020,7 @@ georss-qld-bushfire-alert-client==0.8 getmac==0.9.5 # homeassistant.components.gios -gios==6.0.0 +gios==6.1.0 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13a04de90640a9..c75aca5c8e4474 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -890,7 +890,7 @@ georss-qld-bushfire-alert-client==0.8 getmac==0.9.5 # homeassistant.components.gios -gios==6.0.0 +gios==6.1.0 # homeassistant.components.glances glances-api==0.8.0 diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 49388428805bc0..a4dc0a39be6029 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -1,16 +1,29 @@ """Tests for GIOS.""" -import json from unittest.mock import patch from homeassistant.components.gios.const import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, async_load_fixture +from tests.common import ( + MockConfigEntry, + async_load_json_array_fixture, + async_load_json_object_fixture, +) STATIONS = [ - {"id": 123, "stationName": "Test Name 1", "gegrLat": "99.99", "gegrLon": "88.88"}, - {"id": 321, "stationName": "Test Name 2", "gegrLat": "77.77", "gegrLon": "66.66"}, + { + "Identyfikator stacji": 123, + "Nazwa stacji": "Test Name 1", + "WGS84 φ N": "99.99", + "WGS84 λ E": "88.88", + }, + { + "Identyfikator stacji": 321, + "Nazwa stacji": "Test Name 2", + "WGS84 φ N": "77.77", + "WGS84 λ E": "66.66", + }, ] @@ -26,13 +39,13 @@ async def init_integration( entry_id="86129426118ae32020417a53712d6eef", ) - indexes = json.loads(await async_load_fixture(hass, "indexes.json", DOMAIN)) - station = json.loads(await async_load_fixture(hass, "station.json", DOMAIN)) - sensors = json.loads(await async_load_fixture(hass, "sensors.json", DOMAIN)) + indexes = await async_load_json_object_fixture(hass, "indexes.json", DOMAIN) + station = await async_load_json_array_fixture(hass, "station.json", DOMAIN) + sensors = await async_load_json_object_fixture(hass, "sensors.json", DOMAIN) if incomplete_data: - indexes["stIndexLevel"]["indexLevelName"] = "foo" - sensors["pm10"]["values"][0]["value"] = None - sensors["pm10"]["values"][1]["value"] = None + indexes["AqIndex"] = "foo" + sensors["pm10"]["Lista danych pomiarowych"][0]["Wartość"] = None + sensors["pm10"]["Lista danych pomiarowych"][1]["Wartość"] = None if invalid_indexes: indexes = {} diff --git a/tests/components/gios/fixtures/indexes.json b/tests/components/gios/fixtures/indexes.json index c53d1c78f6e198..1fb46e9a4d86e3 100644 --- a/tests/components/gios/fixtures/indexes.json +++ b/tests/components/gios/fixtures/indexes.json @@ -1,29 +1,38 @@ { - "id": 123, - "stCalcDate": "2020-07-31 15:10:17", - "stIndexLevel": { "id": 1, "indexLevelName": "Dobry" }, - "stSourceDataDate": "2020-07-31 14:00:00", - "so2CalcDate": "2020-07-31 15:10:17", - "so2IndexLevel": { "id": 0, "indexLevelName": "Bardzo dobry" }, - "so2SourceDataDate": "2020-07-31 14:00:00", - "no2CalcDate": 1596201017000, - "no2IndexLevel": { "id": 0, "indexLevelName": "Dobry" }, - "no2SourceDataDate": "2020-07-31 14:00:00", - "coCalcDate": "2020-07-31 15:10:17", - "coIndexLevel": { "id": 0, "indexLevelName": "Dobry" }, - "coSourceDataDate": "2020-07-31 14:00:00", - "pm10CalcDate": "2020-07-31 15:10:17", - "pm10IndexLevel": { "id": 0, "indexLevelName": "Dobry" }, - "pm10SourceDataDate": "2020-07-31 14:00:00", - "pm25CalcDate": "2020-07-31 15:10:17", - "pm25IndexLevel": { "id": 0, "indexLevelName": "Dobry" }, - "pm25SourceDataDate": "2020-07-31 14:00:00", - "o3CalcDate": "2020-07-31 15:10:17", - "o3IndexLevel": { "id": 1, "indexLevelName": "Dobry" }, - "o3SourceDataDate": "2020-07-31 14:00:00", - "c6h6CalcDate": "2020-07-31 15:10:17", - "c6h6IndexLevel": { "id": 0, "indexLevelName": "Bardzo dobry" }, - "c6h6SourceDataDate": "2020-07-31 14:00:00", - "stIndexStatus": true, - "stIndexCrParam": "OZON" + "AqIndex": { + "Identyfikator stacji pomiarowej": 123, + "Data wykonania obliczeń indeksu": "2020-07-31 15:10:17", + "Nazwa kategorii indeksu": "Dobry", + "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika st": "2020-07-31 14:00:00", + "Data wykonania obliczeń indeksu dla wskaźnika SO2": "2020-07-31 15:10:17", + "Wartość indeksu dla wskaźnika SO2": 0, + "Nazwa kategorii indeksu dla wskażnika SO2": "Bardzo dobry", + "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika SO2": "2020-07-31 14:00:00", + "Data wykonania obliczeń indeksu dla wskaźnika NO2": "2020-07-31 14:00:00", + "Wartość indeksu dla wskaźnika NO2": 0, + "Nazwa kategorii indeksu dla wskażnika NO2": "Dobry", + "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika NO2": "2020-07-31 14:00:00", + "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika CO": "2020-07-31 15:10:17", + "Wartość indeksu dla wskaźnika CO": 0, + "Nazwa kategorii indeksu dla wskażnika CO": "Dobry", + "Data wykonania obliczeń indeksu dla wskaźnika CO": "2020-07-31 14:00:00", + "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika PM10": "2020-07-31 15:10:17", + "Wartość indeksu dla wskaźnika PM10": 0, + "Nazwa kategorii indeksu dla wskażnika PM10": "Dobry", + "Data wykonania obliczeń indeksu dla wskaźnika PM10": "2020-07-31 14:00:00", + "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika PM2.5": "2020-07-31 15:10:17", + "Wartość indeksu dla wskaźnika PM2.5": 0, + "Nazwa kategorii indeksu dla wskażnika PM2.5": "Dobry", + "Data wykonania obliczeń indeksu dla wskaźnika PM2.5": "2020-07-31 14:00:00", + "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika O3": "2020-07-31 15:10:17", + "Wartość indeksu dla wskaźnika O3": 1, + "Nazwa kategorii indeksu dla wskażnika O3": "Dobry", + "Data wykonania obliczeń indeksu dla wskaźnika O3": "2020-07-31 14:00:00", + "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika C6H6": "2020-07-31 15:10:17", + "Wartość indeksu dla wskaźnika C6H6": 0, + "Nazwa kategorii indeksu dla wskażnika C6H6": "Bardzo dobry", + "Data wykonania obliczeń indeksu dla wskaźnika C6H6": "2020-07-31 14:00:00", + "Status indeksu ogólnego dla stacji pomiarowej": true, + "Kod zanieczyszczenia krytycznego": "OZON" + } } diff --git a/tests/components/gios/fixtures/sensors.json b/tests/components/gios/fixtures/sensors.json index db0cf2ff849a1b..0fe387d31269e4 100644 --- a/tests/components/gios/fixtures/sensors.json +++ b/tests/components/gios/fixtures/sensors.json @@ -1,51 +1,51 @@ { "so2": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 4.35478 }, - { "date": "2020-07-31 14:00:00", "value": 4.25478 }, - { "date": "2020-07-31 13:00:00", "value": 4.34309 } + "Lista danych pomiarowych": [ + { "Data": "2020-07-31 15:00:00", "Wartość": 4.35478 }, + { "Data": "2020-07-31 14:00:00", "Wartość": 4.25478 }, + { "Data": "2020-07-31 13:00:00", "Wartość": 4.34309 } ] }, "c6h6": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 0.23789 }, - { "date": "2020-07-31 14:00:00", "value": 0.22789 }, - { "date": "2020-07-31 13:00:00", "value": 0.21315 } + "Lista danych pomiarowych": [ + { "Data": "2020-07-31 15:00:00", "Wartość": 0.23789 }, + { "Data": "2020-07-31 14:00:00", "Wartość": 0.22789 }, + { "Data": "2020-07-31 13:00:00", "Wartość": 0.21315 } ] }, "co": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 251.874 }, - { "date": "2020-07-31 14:00:00", "value": 250.874 }, - { "date": "2020-07-31 13:00:00", "value": 251.097 } + "Lista danych pomiarowych": [ + { "Data": "2020-07-31 15:00:00", "Wartość": 251.874 }, + { "Data": "2020-07-31 14:00:00", "Wartość": 250.874 }, + { "Data": "2020-07-31 13:00:00", "Wartość": 251.097 } ] }, "no2": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 7.13411 }, - { "date": "2020-07-31 14:00:00", "value": 7.33411 }, - { "date": "2020-07-31 13:00:00", "value": 9.32578 } + "Lista danych pomiarowych": [ + { "Data": "2020-07-31 15:00:00", "Wartość": 7.13411 }, + { "Data": "2020-07-31 14:00:00", "Wartość": 7.33411 }, + { "Data": "2020-07-31 13:00:00", "Wartość": 9.32578 } ] }, "o3": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 95.7768 }, - { "date": "2020-07-31 14:00:00", "value": 93.7768 }, - { "date": "2020-07-31 13:00:00", "value": 89.4232 } + "Lista danych pomiarowych": [ + { "Data": "2020-07-31 15:00:00", "Wartość": 95.7768 }, + { "Data": "2020-07-31 14:00:00", "Wartość": 93.7768 }, + { "Data": "2020-07-31 13:00:00", "Wartość": 89.4232 } ] }, "pm2.5": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 4 }, - { "date": "2020-07-31 14:00:00", "value": 4 }, - { "date": "2020-07-31 13:00:00", "value": 5 } + "Lista danych pomiarowych": [ + { "Data": "2020-07-31 15:00:00", "Wartość": 4 }, + { "Data": "2020-07-31 14:00:00", "Wartość": 4 }, + { "Data": "2020-07-31 13:00:00", "Wartość": 5 } ] }, "pm10": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 16.8344 }, - { "date": "2020-07-31 14:00:00", "value": 17.8344 }, - { "date": "2020-07-31 13:00:00", "value": 20.8094 } + "Lista danych pomiarowych": [ + { "Data": "2020-07-31 15:00:00", "Wartość": 16.8344 }, + { "Data": "2020-07-31 14:00:00", "Wartość": 17.8344 }, + { "Data": "2020-07-31 13:00:00", "Wartość": 20.8094 } ] } } diff --git a/tests/components/gios/fixtures/station.json b/tests/components/gios/fixtures/station.json index 16cd824a48941d..167e4db3aeecf4 100644 --- a/tests/components/gios/fixtures/station.json +++ b/tests/components/gios/fixtures/station.json @@ -1,72 +1,58 @@ [ { - "id": 672, - "stationId": 117, - "param": { - "paramName": "dwutlenek siarki", - "paramFormula": "SO2", - "paramCode": "SO2", - "idParam": 1 - } + "Identyfikator stanowiska": 672, + "Identyfikator stacji": 117, + "Wskaźnik": "dwutlenek siarki", + "Wskaźnik - wzór": "SO2", + "Wskaźnik - kod": "SO2", + "Id wskaźnika": 1 }, { - "id": 658, - "stationId": 117, - "param": { - "paramName": "benzen", - "paramFormula": "C6H6", - "paramCode": "C6H6", - "idParam": 10 - } + "Identyfikator stanowiska": 658, + "Identyfikator stacji": 117, + "Wskaźnik": "benzen", + "Wskaźnik - wzór": "C6H6", + "Wskaźnik - kod": "C6H6", + "Id wskaźnika": 10 }, { - "id": 660, - "stationId": 117, - "param": { - "paramName": "tlenek węgla", - "paramFormula": "CO", - "paramCode": "CO", - "idParam": 8 - } + "Identyfikator stanowiska": 660, + "Identyfikator stacji": 117, + "Wskaźnik": "tlenek węgla", + "Wskaźnik - wzór": "CO", + "Wskaźnik - kod": "CO", + "Id wskaźnika": 8 }, { - "id": 665, - "stationId": 117, - "param": { - "paramName": "dwutlenek azotu", - "paramFormula": "NO2", - "paramCode": "NO2", - "idParam": 6 - } + "Identyfikator stanowiska": 665, + "Identyfikator stacji": 117, + "Wskaźnik": "dwutlenek azotu", + "Wskaźnik - wzór": "NO2", + "Wskaźnik - kod": "NO2", + "Id wskaźnika": 6 }, { - "id": 667, - "stationId": 117, - "param": { - "paramName": "ozon", - "paramFormula": "O3", - "paramCode": "O3", - "idParam": 5 - } + "Identyfikator stanowiska": 667, + "Identyfikator stacji": 117, + "Wskaźnik": "ozon", + "Wskaźnik - wzór": "O3", + "Wskaźnik - kod": "O3", + "Id wskaźnika": 5 }, { - "id": 670, - "stationId": 117, - "param": { - "paramName": "pył zawieszony PM2.5", - "paramFormula": "PM2.5", - "paramCode": "PM2.5", - "idParam": 69 - } + "Identyfikator stanowiska": 670, + "Identyfikator stacji": 117, + "Wskaźnik": "pył zawieszony PM2.5", + "Wskaźnik - wzór": "PM2.5", + "Wskaźnik - kod": "PM2.5", + "Id wskaźnika": 69 }, { - "id": 14395, - "stationId": 117, - "param": { - "paramName": "pył zawieszony PM10", - "paramFormula": "PM10", - "paramCode": "PM10", - "idParam": 3 - } + "Identyfikator stanowiska": 14395, + "Identyfikator stacji": 117, + "Wskaźnik": "pył zawieszony PM10", + "Wskaźnik - wzór": "PM10", + "Wskaźnik - kod": "PM10", + "Id wskaźnika": 3 } ] diff --git a/tests/components/gios/snapshots/test_diagnostics.ambr b/tests/components/gios/snapshots/test_diagnostics.ambr index 890edc0048260e..4095bf8bf533f2 100644 --- a/tests/components/gios/snapshots/test_diagnostics.ambr +++ b/tests/components/gios/snapshots/test_diagnostics.ambr @@ -42,12 +42,14 @@ 'name': 'carbon monoxide', 'value': 251.874, }), + 'no': None, 'no2': dict({ 'id': 665, 'index': 'good', 'name': 'nitrogen dioxide', 'value': 7.13411, }), + 'nox': None, 'o3': dict({ 'id': 667, 'index': 'good', From 672ffa598420c1c4f3d11fde393995982a6ad222 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 08:01:48 -0500 Subject: [PATCH 25/96] Restore httpx compatibility for non-primitive REST query parameters (#148286) --- homeassistant/components/rest/data.py | 10 ++ tests/components/rest/test_sensor.py | 127 ++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 7f346277314678..3341f296fb9eec 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -115,6 +115,16 @@ async def async_update(self, log_errors: bool = True) -> None: for key, value in rendered_params.items(): if isinstance(value, bool): rendered_params[key] = str(value).lower() + elif not isinstance(value, (str, int, float, type(None))): + # For backward compatibility with httpx behavior, convert non-primitive + # types to strings. This maintains compatibility after switching from + # httpx to aiohttp. See https://github.com/home-assistant/core/issues/148153 + _LOGGER.debug( + "REST query parameter '%s' has type %s, converting to string", + key, + type(value).__name__, + ) + rendered_params[key] = str(value) _LOGGER.debug("Updating from %s", self._resource) # Create request kwargs diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index cbc77e9d53fb2f..b830d6b7743687 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the REST sensor platform.""" from http import HTTPStatus +import logging import ssl from unittest.mock import patch @@ -19,6 +20,14 @@ ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, + CONF_DEVICE_CLASS, + CONF_FORCE_UPDATE, + CONF_METHOD, + CONF_NAME, + CONF_PARAMS, + CONF_RESOURCE, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, CONTENT_TYPE_JSON, SERVICE_RELOAD, STATE_UNAVAILABLE, @@ -1066,6 +1075,124 @@ async def test_update_with_failed_get( assert "Empty reply" in caplog.text +async def test_query_param_dict_value( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test dict values in query params are handled for backward compatibility.""" + # Mock response + aioclient_mock.post( + "https://www.envertecportal.com/ApiInverters/QueryTerminalReal", + status=HTTPStatus.OK, + json={"Data": {"QueryResults": [{"POWER": 1500}]}}, + ) + + # This test checks that when template_complex processes a string that looks like + # a dict/list, it converts it to an actual dict/list, which then needs to be + # handled by our backward compatibility code + with caplog.at_level(logging.DEBUG, logger="homeassistant.components.rest.data"): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + CONF_RESOURCE: ( + "https://www.envertecportal.com/ApiInverters/" + "QueryTerminalReal" + ), + CONF_METHOD: "POST", + CONF_PARAMS: { + "page": "1", + "perPage": "20", + "orderBy": "SN", + # When processed by template.render_complex, certain + # strings might be converted to dicts/lists if they + # look like JSON + "whereCondition": ( + "{{ {'STATIONID': 'A6327A17797C1234'} }}" + ), # Template that evaluates to dict + }, + "sensor": [ + { + CONF_NAME: "Solar MPPT1 Power", + CONF_VALUE_TEMPLATE: ( + "{{ value_json.Data.QueryResults[0].POWER }}" + ), + CONF_DEVICE_CLASS: "power", + CONF_UNIT_OF_MEASUREMENT: "W", + CONF_FORCE_UPDATE: True, + "state_class": "measurement", + } + ], + } + ] + }, + ) + await hass.async_block_till_done() + + # The sensor should be created successfully with backward compatibility + assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1 + state = hass.states.get("sensor.solar_mppt1_power") + assert state is not None + assert state.state == "1500" + + # Check that a debug message was logged about the parameter conversion + assert "REST query parameter 'whereCondition' has type" in caplog.text + assert "converting to string" in caplog.text + + +async def test_query_param_json_string_preserved( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test that JSON strings in query params are preserved and not converted to dicts.""" + # Mock response + aioclient_mock.get( + "https://api.example.com/data", + status=HTTPStatus.OK, + json={"value": 42}, + ) + + # Config with JSON string (quoted) - should remain a string + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + CONF_RESOURCE: "https://api.example.com/data", + CONF_METHOD: "GET", + CONF_PARAMS: { + "filter": '{"type": "sensor", "id": 123}', # JSON string + "normal": "value", + }, + "sensor": [ + { + CONF_NAME: "Test Sensor", + CONF_VALUE_TEMPLATE: "{{ value_json.value }}", + } + ], + } + ] + }, + ) + await hass.async_block_till_done() + + # Check the sensor was created + assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1 + state = hass.states.get("sensor.test_sensor") + assert state is not None + assert state.state == "42" + + # Verify the request was made with the JSON string intact + assert len(aioclient_mock.mock_calls) == 1 + method, url, data, headers = aioclient_mock.mock_calls[0] + assert url.query["filter"] == '{"type": "sensor", "id": 123}' + assert url.query["normal"] == "value" + + async def test_reload(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Verify we can reload reset sensors.""" From 4a10370932b1ec01274353f5e817aad328fcf39e Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:40:48 +0200 Subject: [PATCH 26/96] Bump pyenphase to 2.2.1 (#148292) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 8387ecc9c9f6ac..278045001fc1f4 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["pyenphase"], "quality_scale": "platinum", - "requirements": ["pyenphase==2.2.0"], + "requirements": ["pyenphase==2.2.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 8c4850e31627a4..a45d470b0a60ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1962,7 +1962,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==2.2.0 +pyenphase==2.2.1 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c75aca5c8e4474..1c61f8c781198a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1637,7 +1637,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==2.2.0 +pyenphase==2.2.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 14f059c766883246a174ceba98688951f9df7a35 Mon Sep 17 00:00:00 2001 From: jvits227 <133175738+jvits227@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:51:09 -0400 Subject: [PATCH 27/96] Add lamp states to smartthings selector (#148302) Co-authored-by: Joostlek --- homeassistant/components/smartthings/select.py | 5 +++++ tests/components/smartthings/snapshots/test_select.ambr | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/select.py b/homeassistant/components/smartthings/select.py index 99dc7a09f87b88..3106aba5e49b26 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -18,6 +18,11 @@ LAMP_TO_HA = { "extraHigh": "extra_high", + "high": "high", + "mid": "mid", + "low": "low", + "on": "on", + "off": "off", } WASHER_SOIL_LEVEL_TO_HA = { diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index 7dd57e89c6a587..de4587b7ca899a 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -55,7 +55,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'off', }) # --- # name: test_all_entities[da_ks_oven_01061][select.oven_lamp-entry] @@ -112,7 +112,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'high', }) # --- # name: test_all_entities[da_ks_range_0101x][select.vulcan_lamp-entry] From d303a7d17e95f2ca5036068ae602c29312937a0a Mon Sep 17 00:00:00 2001 From: Samuel Xiao <40679757+XiaoLing-git@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:57:30 +0800 Subject: [PATCH 28/96] Fix Switchbot cloud plug mini current unit Issue (#148314) --- homeassistant/components/switchbot_cloud/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switchbot_cloud/sensor.py b/homeassistant/components/switchbot_cloud/sensor.py index 5a424ea78920fc..f93df234289aaf 100644 --- a/homeassistant/components/switchbot_cloud/sensor.py +++ b/homeassistant/components/switchbot_cloud/sensor.py @@ -113,11 +113,11 @@ ), "Plug Mini (US)": ( VOLTAGE_DESCRIPTION, - CURRENT_DESCRIPTION_IN_A, + CURRENT_DESCRIPTION_IN_MA, ), "Plug Mini (JP)": ( VOLTAGE_DESCRIPTION, - CURRENT_DESCRIPTION_IN_A, + CURRENT_DESCRIPTION_IN_MA, ), "Hub 2": ( TEMPERATURE_DESCRIPTION, From 186c4e7038fdff9e5c84d687b2326de61ab62931 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:26:23 +0800 Subject: [PATCH 29/96] Bump pyswitchbot to 0.68.1 (#148335) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 8e727425a2a4ef..5ef7eec9976bd5 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -41,5 +41,5 @@ "iot_class": "local_push", "loggers": ["switchbot"], "quality_scale": "gold", - "requirements": ["PySwitchbot==0.67.0"] + "requirements": ["PySwitchbot==0.68.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a45d470b0a60ae..b5777776929319 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -84,7 +84,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.67.0 +PySwitchbot==0.68.1 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c61f8c781198a..0496d7bdd9affc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.67.0 +PySwitchbot==0.68.1 # homeassistant.components.syncthru PySyncThru==0.8.0 From 2377b136f32570d5be551fb5aff94898b64444f1 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 8 Jul 2025 13:05:16 +0200 Subject: [PATCH 30/96] Handle binary coils with non default mappings in nibe heatpump (#148354) --- .../components/nibe_heatpump/binary_sensor.py | 3 +- .../components/nibe_heatpump/switch.py | 8 +- tests/components/nibe_heatpump/__init__.py | 6 +- .../snapshots/test_binary_sensor.ambr | 97 +++++++++ .../nibe_heatpump/snapshots/test_switch.ambr | 193 ++++++++++++++++++ .../nibe_heatpump/test_binary_sensor.py | 49 +++++ tests/components/nibe_heatpump/test_button.py | 2 +- .../components/nibe_heatpump/test_climate.py | 2 +- tests/components/nibe_heatpump/test_number.py | 2 +- tests/components/nibe_heatpump/test_switch.py | 133 ++++++++++++ 10 files changed, 487 insertions(+), 8 deletions(-) create mode 100644 tests/components/nibe_heatpump/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/nibe_heatpump/snapshots/test_switch.ambr create mode 100644 tests/components/nibe_heatpump/test_binary_sensor.py create mode 100644 tests/components/nibe_heatpump/test_switch.py diff --git a/homeassistant/components/nibe_heatpump/binary_sensor.py b/homeassistant/components/nibe_heatpump/binary_sensor.py index 284e4d835693b1..d49862180bdcd5 100644 --- a/homeassistant/components/nibe_heatpump/binary_sensor.py +++ b/homeassistant/components/nibe_heatpump/binary_sensor.py @@ -39,6 +39,7 @@ class BinarySensor(CoilEntity, BinarySensorEntity): def __init__(self, coordinator: CoilCoordinator, coil: Coil) -> None: """Initialize entity.""" super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + self._on_value = coil.get_mapping_for(1) def _async_read_coil(self, data: CoilData) -> None: - self._attr_is_on = data.value == "ON" + self._attr_is_on = data.value == self._on_value diff --git a/homeassistant/components/nibe_heatpump/switch.py b/homeassistant/components/nibe_heatpump/switch.py index 2daf3fc48ff847..452244f05b58a4 100644 --- a/homeassistant/components/nibe_heatpump/switch.py +++ b/homeassistant/components/nibe_heatpump/switch.py @@ -41,14 +41,16 @@ class Switch(CoilEntity, SwitchEntity): def __init__(self, coordinator: CoilCoordinator, coil: Coil) -> None: """Initialize entity.""" super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + self._on_value = coil.get_mapping_for(1) + self._off_value = coil.get_mapping_for(0) def _async_read_coil(self, data: CoilData) -> None: - self._attr_is_on = data.value == "ON" + self._attr_is_on = data.value == self._on_value async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self._async_write_coil("ON") + await self._async_write_coil(self._on_value) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await self._async_write_coil("OFF") + await self._async_write_coil(self._off_value) diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index 15cd9859d6ed35..e5ce32b2293c96 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -24,6 +24,8 @@ "connection_type": "nibegw", } +MOCK_UNIQUE_ID = "mock_entry_unique_id" + class MockConnection(Connection): """A mock connection class.""" @@ -59,7 +61,9 @@ def mock_coil_update(self, coil_id: int, value: float | str | None): async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> MockConfigEntry: """Add entry and get the coordinator.""" - entry = MockConfigEntry(domain=DOMAIN, title="Dummy", data=data) + entry = MockConfigEntry( + domain=DOMAIN, title="Dummy", data=data, unique_id=MOCK_UNIQUE_ID + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nibe_heatpump/snapshots/test_binary_sensor.ambr b/tests/components/nibe_heatpump/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000000..37dd7a8679ccb9 --- /dev/null +++ b/tests/components/nibe_heatpump/snapshots/test_binary_sensor.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_update[Model.F1255-49239-OFF][binary_sensor.eb101_installed_49239-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.eb101_installed_49239', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'EB101 Installed', + 'platform': 'nibe_heatpump', + 'previous_unique_id': None, + 'suggested_object_id': 'eb101_installed_49239', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'mock_entry_unique_id-49239', + 'unit_of_measurement': None, + }) +# --- +# name: test_update[Model.F1255-49239-OFF][binary_sensor.eb101_installed_49239-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1255 EB101 Installed', + }), + 'context': , + 'entity_id': 'binary_sensor.eb101_installed_49239', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_update[Model.F1255-49239-ON][binary_sensor.eb101_installed_49239-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.eb101_installed_49239', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'EB101 Installed', + 'platform': 'nibe_heatpump', + 'previous_unique_id': None, + 'suggested_object_id': 'eb101_installed_49239', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'mock_entry_unique_id-49239', + 'unit_of_measurement': None, + }) +# --- +# name: test_update[Model.F1255-49239-ON][binary_sensor.eb101_installed_49239-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1255 EB101 Installed', + }), + 'context': , + 'entity_id': 'binary_sensor.eb101_installed_49239', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nibe_heatpump/snapshots/test_switch.ambr b/tests/components/nibe_heatpump/snapshots/test_switch.ambr new file mode 100644 index 00000000000000..01f35bd8a5408b --- /dev/null +++ b/tests/components/nibe_heatpump/snapshots/test_switch.ambr @@ -0,0 +1,193 @@ +# serializer version: 1 +# name: test_update[Model.F1255-48043-ACTIVE][switch.holiday_activated_48043-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.holiday_activated_48043', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Holiday - Activated', + 'platform': 'nibe_heatpump', + 'previous_unique_id': None, + 'suggested_object_id': 'holiday_activated_48043', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'mock_entry_unique_id-48043', + 'unit_of_measurement': None, + }) +# --- +# name: test_update[Model.F1255-48043-ACTIVE][switch.holiday_activated_48043-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1255 Holiday - Activated', + }), + 'context': , + 'entity_id': 'switch.holiday_activated_48043', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_update[Model.F1255-48043-INACTIVE][switch.holiday_activated_48043-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.holiday_activated_48043', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Holiday - Activated', + 'platform': 'nibe_heatpump', + 'previous_unique_id': None, + 'suggested_object_id': 'holiday_activated_48043', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'mock_entry_unique_id-48043', + 'unit_of_measurement': None, + }) +# --- +# name: test_update[Model.F1255-48043-INACTIVE][switch.holiday_activated_48043-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1255 Holiday - Activated', + }), + 'context': , + 'entity_id': 'switch.holiday_activated_48043', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_update[Model.F1255-48071-OFF][switch.flm_1_accessory_48071-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.flm_1_accessory_48071', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'FLM 1 accessory', + 'platform': 'nibe_heatpump', + 'previous_unique_id': None, + 'suggested_object_id': 'flm_1_accessory_48071', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'mock_entry_unique_id-48071', + 'unit_of_measurement': None, + }) +# --- +# name: test_update[Model.F1255-48071-OFF][switch.flm_1_accessory_48071-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1255 FLM 1 accessory', + }), + 'context': , + 'entity_id': 'switch.flm_1_accessory_48071', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_update[Model.F1255-48071-ON][switch.flm_1_accessory_48071-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.flm_1_accessory_48071', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'FLM 1 accessory', + 'platform': 'nibe_heatpump', + 'previous_unique_id': None, + 'suggested_object_id': 'flm_1_accessory_48071', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'mock_entry_unique_id-48071', + 'unit_of_measurement': None, + }) +# --- +# name: test_update[Model.F1255-48071-ON][switch.flm_1_accessory_48071-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1255 FLM 1 accessory', + }), + 'context': , + 'entity_id': 'switch.flm_1_accessory_48071', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nibe_heatpump/test_binary_sensor.py b/tests/components/nibe_heatpump/test_binary_sensor.py new file mode 100644 index 00000000000000..30010ac61c4855 --- /dev/null +++ b/tests/components/nibe_heatpump/test_binary_sensor.py @@ -0,0 +1,49 @@ +"""Test the Nibe Heat Pump binary sensor entities.""" + +from typing import Any +from unittest.mock import patch + +from nibe.heatpump import Model +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import async_add_model + +from tests.common import snapshot_platform + + +@pytest.fixture(autouse=True) +async def fixture_single_platform(): + """Only allow this platform to load.""" + with patch( + "homeassistant.components.nibe_heatpump.PLATFORMS", [Platform.BINARY_SENSOR] + ): + yield + + +@pytest.mark.parametrize( + ("model", "address", "value"), + [ + (Model.F1255, 49239, "OFF"), + (Model.F1255, 49239, "ON"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + model: Model, + address: int, + value: Any, + coils: dict[int, Any], + snapshot: SnapshotAssertion, +) -> None: + """Test setting of value.""" + coils[address] = value + + entry = await async_add_model(hass, model) + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) diff --git a/tests/components/nibe_heatpump/test_button.py b/tests/components/nibe_heatpump/test_button.py index 5015bba4092b89..4f2bab7ad0a511 100644 --- a/tests/components/nibe_heatpump/test_button.py +++ b/tests/components/nibe_heatpump/test_button.py @@ -1,4 +1,4 @@ -"""Test the Nibe Heat Pump config flow.""" +"""Test the Nibe Heat Pump buttons.""" from typing import Any from unittest.mock import AsyncMock, patch diff --git a/tests/components/nibe_heatpump/test_climate.py b/tests/components/nibe_heatpump/test_climate.py index a9620b5ddb31d6..85e932f80189fa 100644 --- a/tests/components/nibe_heatpump/test_climate.py +++ b/tests/components/nibe_heatpump/test_climate.py @@ -1,4 +1,4 @@ -"""Test the Nibe Heat Pump config flow.""" +"""Test the Nibe Heat Pump climate entities.""" from typing import Any from unittest.mock import call, patch diff --git a/tests/components/nibe_heatpump/test_number.py b/tests/components/nibe_heatpump/test_number.py index dc7faf0a80e32c..e054979b7a9bd0 100644 --- a/tests/components/nibe_heatpump/test_number.py +++ b/tests/components/nibe_heatpump/test_number.py @@ -1,4 +1,4 @@ -"""Test the Nibe Heat Pump config flow.""" +"""Test the Nibe Heat Pump number entities.""" from typing import Any from unittest.mock import AsyncMock, patch diff --git a/tests/components/nibe_heatpump/test_switch.py b/tests/components/nibe_heatpump/test_switch.py new file mode 100644 index 00000000000000..4221de52ba121f --- /dev/null +++ b/tests/components/nibe_heatpump/test_switch.py @@ -0,0 +1,133 @@ +"""Test the Nibe Heat Pump switch entities.""" + +from typing import Any +from unittest.mock import AsyncMock, patch + +from nibe.coil import CoilData +from nibe.heatpump import Model +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_PLATFORM, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import async_add_model + +from tests.common import snapshot_platform + + +@pytest.fixture(autouse=True) +async def fixture_single_platform(): + """Only allow this platform to load.""" + with patch("homeassistant.components.nibe_heatpump.PLATFORMS", [Platform.SWITCH]): + yield + + +@pytest.mark.parametrize( + ("model", "address", "value"), + [ + (Model.F1255, 48043, "INACTIVE"), + (Model.F1255, 48043, "ACTIVE"), + (Model.F1255, 48071, "OFF"), + (Model.F1255, 48071, "ON"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + model: Model, + address: int, + value: Any, + coils: dict[int, Any], + snapshot: SnapshotAssertion, +) -> None: + """Test setting of value.""" + coils[address] = value + + entry = await async_add_model(hass, model) + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +@pytest.mark.parametrize( + ("model", "address", "entity_id", "state"), + [ + (Model.F1255, 48043, "switch.holiday_activated_48043", "INACTIVE"), + (Model.F1255, 48071, "switch.flm_1_accessory_48071", "OFF"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_turn_on( + hass: HomeAssistant, + mock_connection: AsyncMock, + model: Model, + entity_id: str, + address: int, + state: Any, + coils: dict[int, Any], +) -> None: + """Test setting of value.""" + coils[address] = state + + await async_add_model(hass, model) + + # Write value + await hass.services.async_call( + SWITCH_PLATFORM, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + # Verify written + args = mock_connection.write_coil.call_args + assert args + coil = args.args[0] + assert isinstance(coil, CoilData) + assert coil.coil.address == address + assert coil.raw_value == 1 + + +@pytest.mark.parametrize( + ("model", "address", "entity_id", "state"), + [ + (Model.F1255, 48043, "switch.holiday_activated_48043", "INACTIVE"), + (Model.F1255, 48071, "switch.flm_1_accessory_48071", "ON"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_turn_off( + hass: HomeAssistant, + mock_connection: AsyncMock, + model: Model, + entity_id: str, + address: int, + state: Any, + coils: dict[int, Any], +) -> None: + """Test setting of value.""" + coils[address] = state + + await async_add_model(hass, model) + + # Write value + await hass.services.async_call( + SWITCH_PLATFORM, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + # Verify written + args = mock_connection.write_coil.call_args + assert args + coil = args.args[0] + assert isinstance(coil, CoilData) + assert coil.coil.address == address + assert coil.raw_value == 0 From 91cdf1a367da31758b35dde4f3765f7882c74213 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 8 Jul 2025 11:14:41 +0300 Subject: [PATCH 31/96] Bump aioamazondevices to 3.2.8 (#148365) Co-authored-by: Joakim Plate --- homeassistant/components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/alexa_devices/conftest.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 70281390436805..34fdd1448a5364 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.2.3"] + "requirements": ["aioamazondevices==3.2.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index b5777776929319..dada15791d280e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.2.3 +aioamazondevices==3.2.8 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0496d7bdd9affc..38da863b93a5ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.2.3 +aioamazondevices==3.2.8 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index 798515505289c2..a5a49a343a903b 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -50,6 +50,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: device_type="echo", device_owner_customer_id="amazon_ower_id", device_cluster_members=[TEST_SERIAL_NUMBER], + device_locale="en-US", online=True, serial_number=TEST_SERIAL_NUMBER, software_version="echo_test_software_version", From 3b047859f92a318470efba72660c33f8c630ab96 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 8 Jul 2025 13:06:05 +0200 Subject: [PATCH 32/96] Create own clientsession for lamarzocco (#148385) --- homeassistant/components/lamarzocco/__init__.py | 5 ++--- homeassistant/components/lamarzocco/config_flow.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index ff977438f386d5..2d68b3be345480 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -23,7 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import CONF_USE_BLUETOOTH, DOMAIN from .coordinator import ( @@ -57,11 +57,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - assert entry.unique_id serial = entry.unique_id - client = async_get_clientsession(hass) cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], - client=client, + client=async_create_clientsession(hass), ) try: diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index 8cb2e4dfc61a3d..e352e337d0be73 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -33,7 +33,7 @@ ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -83,7 +83,7 @@ async def async_step_user( **user_input, } - self._client = async_get_clientsession(self.hass) + self._client = async_create_clientsession(self.hass) cloud_client = LaMarzoccoCloudClient( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], From 435465e5695bf7fb96b187fd28a9db785148123c Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 8 Jul 2025 13:06:43 +0200 Subject: [PATCH 33/96] Bump pylamarzocco to 2.0.11 (#148386) --- homeassistant/components/lamarzocco/binary_sensor.py | 2 +- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 4fc2c0b05df1de..afbb779b696f67 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -66,7 +66,7 @@ class LaMarzoccoBinarySensorEntityDescription( WidgetType.CM_BACK_FLUSH, BackFlush(status=BackFlushStatus.OFF) ), ).status - is BackFlushStatus.REQUESTED + in (BackFlushStatus.REQUESTED, BackFlushStatus.CLEANING) ), entity_category=EntityCategory.DIAGNOSTIC, supported_fn=lambda coordinator: ( diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 10cb23146ae52e..3c070769b5b092 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.10"] + "requirements": ["pylamarzocco==2.0.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index dada15791d280e..d17b1030922a06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2100,7 +2100,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.10 +pylamarzocco==2.0.11 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38da863b93a5ee..6ad4aa867bf511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1745,7 +1745,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.10 +pylamarzocco==2.0.11 # homeassistant.components.lastfm pylast==5.1.0 From d51a44acbc4f39c28b7a6c5e165a5112a08759c9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 8 Jul 2025 12:34:51 +0200 Subject: [PATCH 34/96] Bump pySmartThings to 3.2.7 (#148394) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 7c3fc47e512769..2c4974a65672dc 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.5"] + "requirements": ["pysmartthings==3.2.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index d17b1030922a06..b15b6207ac0314 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2348,7 +2348,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.0 # homeassistant.components.smartthings -pysmartthings==3.2.5 +pysmartthings==3.2.7 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ad4aa867bf511..087be4dc6452f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1951,7 +1951,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.0 # homeassistant.components.smartthings -pysmartthings==3.2.5 +pysmartthings==3.2.7 # homeassistant.components.smarty pysmarty2==0.10.2 From b8425de0d00d69d1950a5cbd50f4972226966ce5 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:37:45 +0200 Subject: [PATCH 35/96] Bump uiprotect to version 7.14.2 (#148453) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 47e2a01e798b3c..8243a55d779532 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.14.1", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.14.2", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index b15b6207ac0314..c7cb2d62836721 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2994,7 +2994,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.14.1 +uiprotect==7.14.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 087be4dc6452f1..9c8acdcd53538a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2468,7 +2468,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.14.1 +uiprotect==7.14.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From b6d316c8f2430a9a513ecdd275fb92a4c16a000c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 9 Jul 2025 11:30:43 +0100 Subject: [PATCH 36/96] Bump hass-nabucasa from 0.105.0 to 0.106.0 (#148473) --- 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 0d44d57ac5ea7f..7c64100873c936 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.105.0"], + "requirements": ["hass-nabucasa==0.106.0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 824c3d945fe93e..6c6eac1f6ddfce 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==3.49.0 -hass-nabucasa==0.105.0 +hass-nabucasa==0.106.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250702.1 diff --git a/pyproject.toml b/pyproject.toml index b4478a69d53f77..4b9140296a7e5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.105.0", + "hass-nabucasa==0.106.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", diff --git a/requirements.txt b/requirements.txt index 011d76e66b50c7..61aeaf81d140ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.105.0 +hass-nabucasa==0.106.0 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c7cb2d62836721..5218bb9068a96b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,7 +1127,7 @@ habiticalib==0.4.0 habluetooth==3.49.0 # homeassistant.components.cloud -hass-nabucasa==0.105.0 +hass-nabucasa==0.106.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c8acdcd53538a..aabad61a45d9f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ habiticalib==0.4.0 habluetooth==3.49.0 # homeassistant.components.cloud -hass-nabucasa==0.105.0 +hass-nabucasa==0.106.0 # homeassistant.components.assist_satellite # homeassistant.components.conversation From 00e2a177a5a432c4c2f668fe464c3ac23428f416 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 9 Jul 2025 13:53:15 +0200 Subject: [PATCH 37/96] Revert "Deprecate hddtemp" (#148482) --- homeassistant/components/hddtemp/__init__.py | 2 -- homeassistant/components/hddtemp/sensor.py | 20 +------------------ tests/components/hddtemp/test_sensor.py | 21 ++------------------ 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/hddtemp/__init__.py b/homeassistant/components/hddtemp/__init__.py index 66a819f1e8d7a4..121238df9fe2f2 100644 --- a/homeassistant/components/hddtemp/__init__.py +++ b/homeassistant/components/hddtemp/__init__.py @@ -1,3 +1 @@ """The hddtemp component.""" - -DOMAIN = "hddtemp" diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 192ddffd330f36..4d9bbeb9516757 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -22,14 +22,11 @@ CONF_PORT, UnitOfTemperature, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN - _LOGGER = logging.getLogger(__name__) ATTR_DEVICE = "device" @@ -59,21 +56,6 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the HDDTemp sensor.""" - create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_system_packages_yaml_integration_{DOMAIN}", - breaks_in_ha_version="2025.12.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_system_packages_yaml_integration", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "hddtemp", - }, - ) - name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 62882c7df8bd8f..56ad9fdcb0e13f 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,15 +1,12 @@ """The tests for the hddtemp platform.""" import socket -from unittest.mock import Mock, patch +from unittest.mock import patch import pytest -from homeassistant.components.hddtemp import DOMAIN -from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import UnitOfTemperature -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import issue_registry as ir +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} @@ -195,17 +192,3 @@ async def test_hddtemp_host_unreachable(hass: HomeAssistant, telnetmock) -> None assert await async_setup_component(hass, "sensor", VALID_CONFIG_HOST_UNREACHABLE) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - - -@patch.dict("sys.modules", gsp=Mock()) -async def test_repair_issue_is_created( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test repair issue is created.""" - assert await async_setup_component(hass, PLATFORM_DOMAIN, VALID_CONFIG_MINIMAL) - await hass.async_block_till_done() - assert ( - HOMEASSISTANT_DOMAIN, - f"deprecated_system_packages_yaml_integration_{DOMAIN}", - ) in issue_registry.issues From e951fc401c792d570356253a7b229b214e5f821f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 11 Jul 2025 11:19:54 +0200 Subject: [PATCH 38/96] Fix entity_id should be based on object_id the first time an entity is added (#148484) --- homeassistant/components/mqtt/entity.py | 35 ++++++++++++------- tests/components/mqtt/test_discovery.py | 46 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mqtt/entity.py b/homeassistant/components/mqtt/entity.py index 338779f32cb614..f1594a7b03419d 100644 --- a/homeassistant/components/mqtt/entity.py +++ b/homeassistant/components/mqtt/entity.py @@ -389,16 +389,6 @@ def _async_setup_entities() -> None: _async_setup_entities() -def init_entity_id_from_config( - hass: HomeAssistant, entity: Entity, config: ConfigType, entity_id_format: str -) -> None: - """Set entity_id from object_id if defined in config.""" - if CONF_OBJECT_ID in config: - entity.entity_id = async_generate_entity_id( - entity_id_format, config[CONF_OBJECT_ID], None, hass - ) - - class MqttAttributesMixin(Entity): """Mixin used for platforms that support JSON attributes.""" @@ -1312,6 +1302,7 @@ class MqttEntity( _attr_should_poll = False _default_name: str | None _entity_id_format: str + _update_registry_entity_id: str | None = None def __init__( self, @@ -1346,13 +1337,33 @@ def __init__( def _init_entity_id(self) -> None: """Set entity_id from object_id if defined in config.""" - init_entity_id_from_config( - self.hass, self, self._config, self._entity_id_format + if CONF_OBJECT_ID not in self._config: + return + self.entity_id = async_generate_entity_id( + self._entity_id_format, self._config[CONF_OBJECT_ID], None, self.hass ) + if self.unique_id is None: + return + # Check for previous deleted entities + entity_registry = er.async_get(self.hass) + entity_platform = self._entity_id_format.split(".")[0] + if ( + deleted_entry := entity_registry.deleted_entities.get( + (entity_platform, DOMAIN, self.unique_id) + ) + ) and deleted_entry.entity_id != self.entity_id: + # Plan to update the entity_id basis on `object_id` if a deleted entity was found + self._update_registry_entity_id = self.entity_id @final async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" + if self._update_registry_entity_id is not None: + entity_registry = er.async_get(self.hass) + entity_registry.async_update_entity( + self.entity_id, new_entity_id=self._update_registry_entity_id + ) + await super().async_added_to_hass() self._subscriptions = {} self._prepare_subscribe_topics() diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 35a9a0494a601b..04b4bda0d794e9 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1496,6 +1496,52 @@ async def test_discovery_with_object_id( assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered +async def test_discovery_with_object_id_for_previous_deleted_entity( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test discovering an MQTT entity with object_id and unique_id.""" + + topic = "homeassistant/sensor/object/bla/config" + config = ( + '{ "name": "Hello World 11", "unique_id": "very_unique", ' + '"obj_id": "hello_id", "state_topic": "test-topic" }' + ) + new_config = ( + '{ "name": "Hello World 11", "unique_id": "very_unique", ' + '"obj_id": "updated_hello_id", "state_topic": "test-topic" }' + ) + initial_entity_id = "sensor.hello_id" + new_entity_id = "sensor.updated_hello_id" + name = "Hello World 11" + domain = "sensor" + + await mqtt_mock_entry() + async_fire_mqtt_message(hass, topic, config) + await hass.async_block_till_done() + + state = hass.states.get(initial_entity_id) + + assert state is not None + assert state.name == name + assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered + + # Delete the entity + async_fire_mqtt_message(hass, topic, "") + await hass.async_block_till_done() + assert (domain, "object bla") not in hass.data["mqtt"].discovery_already_discovered + + # Rediscover with new object_id + async_fire_mqtt_message(hass, topic, new_config) + await hass.async_block_till_done() + + state = hass.states.get(new_entity_id) + + assert state is not None + assert state.name == name + assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered + + async def test_discovery_incl_nodeid( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: From 9c9836defdc1451939ccb9b518edd078b8fd5e76 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:22:31 +0200 Subject: [PATCH 39/96] Bump aioimmich to 0.10.2 (#148503) --- homeassistant/components/immich/manifest.json | 2 +- homeassistant/components/immich/update.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index 80dcd87cd88005..906356a4bc9f2f 100644 --- a/homeassistant/components/immich/manifest.json +++ b/homeassistant/components/immich/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aioimmich"], "quality_scale": "silver", - "requirements": ["aioimmich==0.10.1"] + "requirements": ["aioimmich==0.10.2"] } diff --git a/homeassistant/components/immich/update.py b/homeassistant/components/immich/update.py index 9955e355c96f6c..e0af5c1c67f1e2 100644 --- a/homeassistant/components/immich/update.py +++ b/homeassistant/components/immich/update.py @@ -44,7 +44,7 @@ def installed_version(self) -> str: return self.coordinator.data.server_about.version @property - def latest_version(self) -> str: + def latest_version(self) -> str | None: """Available new immich server version.""" assert self.coordinator.data.server_version_check return self.coordinator.data.server_version_check.release_version diff --git a/requirements_all.txt b/requirements_all.txt index 5218bb9068a96b..b431371da33612 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -283,7 +283,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.10.1 +aioimmich==0.10.2 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aabad61a45d9f5..5f5dd955b63bfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.10.1 +aioimmich==0.10.2 # homeassistant.components.apache_kafka aiokafka==0.10.0 From 962ad99c205bcce3ffb9cca5210c63fde5ba9880 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:44:50 +0200 Subject: [PATCH 40/96] Add workaround for sub units without main device in AVM Fritz!SmartHome (#148507) --- .../components/fritzbox/coordinator.py | 13 ++++-- tests/components/fritzbox/test_coordinator.py | 46 ++++++++++++++++++- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 8a37ebf63e49e1..a95af62da6c27d 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -171,14 +171,19 @@ async def _async_update_data(self) -> FritzboxCoordinatorData: for device in new_data.devices.values(): # create device registry entry for new main devices - if ( - device.ain not in self.data.devices - and device.device_and_unit_id[1] is None + if device.ain not in self.data.devices and ( + device.device_and_unit_id[1] is None + or ( + # workaround for sub units without a main device, e.g. Energy 250 + # https://github.com/home-assistant/core/issues/145204 + device.device_and_unit_id[1] == "1" + and device.device_and_unit_id[0] not in new_data.devices + ) ): dr.async_get(self.hass).async_get_or_create( config_entry_id=self.config_entry.entry_id, name=device.name, - identifiers={(DOMAIN, device.ain)}, + identifiers={(DOMAIN, device.device_and_unit_id[0])}, manufacturer=device.manufacturer, model=device.productname, sw_version=device.fw_version, diff --git a/tests/components/fritzbox/test_coordinator.py b/tests/components/fritzbox/test_coordinator.py index 61de0c99940283..794d6ac439714d 100644 --- a/tests/components/fritzbox/test_coordinator.py +++ b/tests/components/fritzbox/test_coordinator.py @@ -15,7 +15,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow -from . import FritzDeviceCoverMock, FritzDeviceSwitchMock, FritzEntityBaseMock +from . import ( + FritzDeviceCoverMock, + FritzDeviceSensorMock, + FritzDeviceSwitchMock, + FritzEntityBaseMock, +) from .const import MOCK_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed @@ -140,3 +145,42 @@ async def test_coordinator_automatic_registry_cleanup( assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 12 assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 1 + + +async def test_coordinator_workaround_sub_units_without_main_device( + hass: HomeAssistant, + fritz: Mock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test the workaround for sub units without main device.""" + fritz().get_devices.return_value = [ + FritzDeviceSensorMock( + ain="bad_device-1", + device_and_unit_id=("bad_device", "1"), + name="bad_sensor_sub", + ), + FritzDeviceSensorMock( + ain="good_device", + device_and_unit_id=("good_device", None), + name="good_sensor", + ), + FritzDeviceSensorMock( + ain="good_device-1", + device_and_unit_id=("good_device", "1"), + name="good_sensor_sub", + ), + ] + + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + assert len(device_entries) == 2 + assert device_entries[0].identifiers == {(DOMAIN, "good_device")} + assert device_entries[1].identifiers == {(DOMAIN, "bad_device")} From 0990cef917f28b604b7c78ffeab6cf3c0c10149b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 10 Jul 2025 00:26:13 +0200 Subject: [PATCH 41/96] Add Home Connect resume command button when an appliance is paused (#148512) --- .../components/home_connect/coordinator.py | 30 ++++++++- tests/components/home_connect/test_button.py | 63 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/home_connect/coordinator.py b/homeassistant/components/home_connect/coordinator.py index 3c9d33424a884f..76faaefa9318d3 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -41,7 +41,12 @@ from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import API_DEFAULT_RETRY_AFTER, APPLIANCES_WITH_PROGRAMS, DOMAIN +from .const import ( + API_DEFAULT_RETRY_AFTER, + APPLIANCES_WITH_PROGRAMS, + BSH_OPERATION_STATE_PAUSE, + DOMAIN, +) from .utils import get_dict_from_home_connect_error _LOGGER = logging.getLogger(__name__) @@ -66,6 +71,7 @@ class HomeConnectApplianceData: def update(self, other: HomeConnectApplianceData) -> None: """Update data with data from other instance.""" + self.commands.clear() self.commands.update(other.commands) self.events.update(other.events) self.info.connected = other.info.connected @@ -201,6 +207,28 @@ async def _event_listener(self) -> None: # noqa: C901 raw_key=status_key.value, value=event.value, ) + if ( + status_key == StatusKey.BSH_COMMON_OPERATION_STATE + and event.value == BSH_OPERATION_STATE_PAUSE + and CommandKey.BSH_COMMON_RESUME_PROGRAM + not in ( + commands := self.data[ + event_message_ha_id + ].commands + ) + ): + # All the appliances that can be paused + # should have the resume command available. + commands.add(CommandKey.BSH_COMMON_RESUME_PROGRAM) + for ( + listener, + context, + ) in self._special_listeners.values(): + if ( + EventKey.BSH_COMMON_APPLIANCE_DEPAIRED + not in context + ): + listener() self._call_event_listener(event_message) case EventType.NOTIFY: diff --git a/tests/components/home_connect/test_button.py b/tests/components/home_connect/test_button.py index ee4d5f1d729968..e61ec5e2b1fa87 100644 --- a/tests/components/home_connect/test_button.py +++ b/tests/components/home_connect/test_button.py @@ -1,12 +1,14 @@ """Tests for home_connect button entities.""" from collections.abc import Awaitable, Callable -from typing import Any +from typing import Any, cast from unittest.mock import AsyncMock, MagicMock from aiohomeconnect.model import ( ArrayOfCommands, CommandKey, + Event, + EventKey, EventMessage, HomeAppliance, ) @@ -317,3 +319,62 @@ async def test_stop_program_button_exception( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) + + +@pytest.mark.parametrize("appliance", ["Washer"], indirect=True) +async def test_enable_resume_command_on_pause( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, +) -> None: + """Test if all commands enabled option works as expected.""" + entity_id = "button.washer_resume_program" + + original_get_available_commands = client.get_available_commands + + async def get_available_commands_side_effect(ha_id: str) -> ArrayOfCommands: + array_of_commands = cast( + ArrayOfCommands, await original_get_available_commands(ha_id) + ) + if ha_id == appliance.ha_id: + for command in array_of_commands.commands: + if command.key == CommandKey.BSH_COMMON_RESUME_PROGRAM: + # Simulate that the resume command is not available initially + array_of_commands.commands.remove(command) + break + return array_of_commands + + client.get_available_commands = AsyncMock( + side_effect=get_available_commands_side_effect + ) + + assert await integration_setup(client) + assert config_entry.state is ConfigEntryState.LOADED + + assert not hass.states.get(entity_id) + + await client.add_events( + [ + EventMessage( + appliance.ha_id, + EventType.STATUS, + data=ArrayOfEvents( + [ + Event( + key=EventKey.BSH_COMMON_STATUS_OPERATION_STATE, + raw_key=EventKey.BSH_COMMON_STATUS_OPERATION_STATE.value, + timestamp=0, + level="", + handling="", + value="BSH.Common.EnumType.OperationState.Pause", + ) + ] + ), + ) + ] + ) + await hass.async_block_till_done() + + assert hass.states.get(entity_id) From d87379d0832109f681cc08a88556e414c25f2a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 10 Jul 2025 09:35:30 +0200 Subject: [PATCH 42/96] Use the link to the issue instead of creating new issues at Home Connect (#148523) --- homeassistant/components/home_connect/coordinator.py | 5 +---- homeassistant/components/home_connect/strings.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/home_connect/coordinator.py b/homeassistant/components/home_connect/coordinator.py index 76faaefa9318d3..bb419f6bd7cd0f 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -655,10 +655,7 @@ def refreshed_too_often_recently(self, appliance_ha_id: str) -> bool: "times": str(MAX_EXECUTIONS), "time_window": str(MAX_EXECUTIONS_TIME_WINDOW // 60), "home_connect_resource_url": "https://www.home-connect.com/global/help-support/error-codes#/Togglebox=15362315-13320636-1/", - "home_assistant_core_new_issue_url": ( - "https://github.com/home-assistant/core/issues/new?template=bug_report.yml" - f"&integration_name={DOMAIN}&integration_link=https://www.home-assistant.io/integrations/{DOMAIN}/" - ), + "home_assistant_core_issue_url": "https://github.com/home-assistant/core/issues/147299", }, ) return True diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 99c89ec8788b00..e1c0b42ca0bfd0 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -130,7 +130,7 @@ "step": { "confirm": { "title": "[%key:component::home_connect::issues::home_connect_too_many_connected_paired_events::title%]", - "description": "The appliance \"{appliance_name}\" has been reported as connected or paired {times} times in less than {time_window} minutes, so refreshes on connected or paired events has been disabled to avoid exceeding the API rate limit.\n\nPlease refer to the [Home Connect Wi-Fi requirements and recommendations]({home_connect_resource_url}). If everything seems right with your network configuration, restart the appliance.\n\nClick \"submit\" to re-enable the updates.\nIf the issue persists, please create an issue in the [Home Assistant core repository]({home_assistant_core_new_issue_url})." + "description": "The appliance \"{appliance_name}\" has been reported as connected or paired {times} times in less than {time_window} minutes, so refreshes on connected or paired events has been disabled to avoid exceeding the API rate limit.\n\nPlease refer to the [Home Connect Wi-Fi requirements and recommendations]({home_connect_resource_url}). If everything seems right with your network configuration, restart the appliance.\n\nClick \"submit\" to re-enable the updates.\nIf the issue persists, please see the following issue in the [Home Assistant core repository]({home_assistant_core_issue_url})." } } } From 63b21fda1a1783dfe102e4bff20729b2e8aa0b22 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 11 Jul 2025 09:06:18 +0200 Subject: [PATCH 43/96] Ensure response is fully read to prevent premature connection closure in rest command (#148532) --- .../components/rest_command/__init__.py | 5 ++++ tests/components/rest_command/test_init.py | 26 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index c6a4206de4a2ec..32eb1aae2b0734 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -178,6 +178,11 @@ async def async_service_handler(service: ServiceCall) -> ServiceResponse: ) if not service.return_response: + # always read the response to avoid closing the connection + # before the server has finished sending it, while avoiding excessive memory usage + async for _ in response.content.iter_chunked(1024): + pass + return None _content = None diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 97ef29dfacafcc..6ba3da8b86732b 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -326,7 +326,7 @@ async def test_rest_command_get_response_malformed_json( aioclient_mock.get( TEST_URL, - content='{"status": "failure", 42', + content=b'{"status": "failure", 42', headers={"content-type": "application/json"}, ) @@ -379,3 +379,27 @@ async def test_rest_command_get_response_none( ) assert not response + + +async def test_rest_command_response_iter_chunked( + hass: HomeAssistant, + setup_component: ComponentSetup, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Ensure response is consumed when return_response is False.""" + await setup_component() + + png = base64.decodebytes( + b"iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAIAAAB7QOjdAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQ" + b"UAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAPSURBVBhXY/h/ku////8AECAE1JZPvDAAAAAASUVORK5CYII=" + ) + aioclient_mock.get(TEST_URL, content=png) + + with patch("aiohttp.StreamReader.iter_chunked", autospec=True) as mock_iter_chunked: + response = await hass.services.async_call(DOMAIN, "get_test", {}, blocking=True) + + # Ensure the response is not returned + assert response is None + + # Verify iter_chunked was called with a chunk size + assert mock_iter_chunked.called From 5cf5be8c9cc03e35c6812144a2b35aae70b0a139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristof=20Mari=C3=ABn?= Date: Thu, 10 Jul 2025 10:52:03 +0200 Subject: [PATCH 44/96] Fix for Renson set Breeze fan speed (#148537) --- homeassistant/components/renson/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/renson/fan.py b/homeassistant/components/renson/fan.py index 474ab64094374b..c82cad012c31b2 100644 --- a/homeassistant/components/renson/fan.py +++ b/homeassistant/components/renson/fan.py @@ -196,7 +196,7 @@ async def async_set_percentage(self, percentage: int) -> None: all_data = self.coordinator.data breeze_temp = self.api.get_field_value(all_data, BREEZE_TEMPERATURE_FIELD) await self.hass.async_add_executor_job( - self.api.set_breeze, cmd.name, breeze_temp, True + self.api.set_breeze, cmd, breeze_temp, True ) else: await self.hass.async_add_executor_job(self.api.set_manual_level, cmd) From 3c2fa023b430cb75ade1537d52764a36e30f5fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 10 Jul 2025 12:09:24 +0200 Subject: [PATCH 45/96] Remove vg argument from miele auth flow (#148541) --- homeassistant/components/miele/config_flow.py | 8 -------- tests/components/miele/test_config_flow.py | 4 ---- 2 files changed, 12 deletions(-) diff --git a/homeassistant/components/miele/config_flow.py b/homeassistant/components/miele/config_flow.py index d3c7dbba12b712..191cd9a0454e05 100644 --- a/homeassistant/components/miele/config_flow.py +++ b/homeassistant/components/miele/config_flow.py @@ -26,14 +26,6 @@ def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) - @property - def extra_authorize_data(self) -> dict: - """Extra data that needs to be appended to the authorize url.""" - # "vg" is mandatory but the value doesn't seem to matter - return { - "vg": "sv-SE", - } - async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: diff --git a/tests/components/miele/test_config_flow.py b/tests/components/miele/test_config_flow.py index bbe5844c1cd855..5ce129b255d9cb 100644 --- a/tests/components/miele/test_config_flow.py +++ b/tests/components/miele/test_config_flow.py @@ -46,7 +46,6 @@ async def test_full_flow( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" f"&state={state}" - "&vg=sv-SE" ) client = await hass_client_no_auth() @@ -118,7 +117,6 @@ async def test_flow_reauth_abort( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" f"&state={state}" - "&vg=sv-SE" ) client = await hass_client_no_auth() @@ -187,7 +185,6 @@ async def test_flow_reconfigure_abort( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" f"&state={state}" - "&vg=sv-SE" ) client = await hass_client_no_auth() @@ -247,7 +244,6 @@ async def test_zeroconf_flow( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" f"&state={state}" - "&vg=sv-SE" ) client = await hass_client_no_auth() From f1272ef513df9e2b23294832ac83f4a6d1ec7f4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Jul 2025 11:10:48 -1000 Subject: [PATCH 46/96] Bump aiohttp to 3.12.14 (#148565) --- homeassistant/components/http/ban.py | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 7e55191639b997..71f3d54bef6abf 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -64,7 +64,7 @@ async def ban_startup(app: Application) -> None: """Initialize bans when app starts up.""" await app[KEY_BAN_MANAGER].async_load() - app.on_startup.append(ban_startup) # type: ignore[arg-type] + app.on_startup.append(ban_startup) @middleware diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6c6eac1f6ddfce..9a02b999775357 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.5.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.12.13 +aiohttp==3.12.14 aiohttp_cors==0.8.1 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 4b9140296a7e5d..17fb74f452c3e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.13", + "aiohttp==3.12.14", "aiohttp_cors==0.8.1", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 61aeaf81d140ce..67c38d690b154e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.5.0 aiohasupervisor==0.3.1 -aiohttp==3.12.13 +aiohttp==3.12.14 aiohttp_cors==0.8.1 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 From dc2736580f5d9508cf3b9cc9baa98fa469cca564 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Jul 2025 23:09:47 +0200 Subject: [PATCH 47/96] Update frontend to 20250702.2 (#148573) --- 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 748d8f0c6f0052..a7582ebc5e25aa 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==20250702.1"] + "requirements": ["home-assistant-frontend==20250702.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9a02b999775357..01bf8e248857cf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.49.0 hass-nabucasa==0.106.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250702.1 +home-assistant-frontend==20250702.2 home-assistant-intents==2025.6.23 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b431371da33612..02520158cd41b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1168,7 +1168,7 @@ hole==0.8.0 holidays==0.75 # homeassistant.components.frontend -home-assistant-frontend==20250702.1 +home-assistant-frontend==20250702.2 # homeassistant.components.conversation home-assistant-intents==2025.6.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f5dd955b63bfc..374fc8cc36b4d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,7 +1017,7 @@ hole==0.8.0 holidays==0.75 # homeassistant.components.frontend -home-assistant-frontend==20250702.1 +home-assistant-frontend==20250702.2 # homeassistant.components.conversation home-assistant-intents==2025.6.23 From 150d4716facf376f156e46567aa181f8775d0f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C6=B0u=20Quang=20V=C5=A9?= Date: Sat, 12 Jul 2025 01:53:38 +0700 Subject: [PATCH 48/96] Fix Google Cloud 504 Deadline Exceeded (#148589) --- homeassistant/components/google_cloud/stt.py | 2 +- homeassistant/components/google_cloud/tts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_cloud/stt.py b/homeassistant/components/google_cloud/stt.py index cd5055383eadae..8a548cde8bb3d2 100644 --- a/homeassistant/components/google_cloud/stt.py +++ b/homeassistant/components/google_cloud/stt.py @@ -127,7 +127,7 @@ async def request_generator() -> AsyncGenerator[ try: responses = await self._client.streaming_recognize( requests=request_generator(), - timeout=10, + timeout=30, retry=AsyncRetry(initial=0.1, maximum=2.0, multiplier=2.0), ) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 16519645deeb5a..817c424d1fc0d2 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -218,7 +218,7 @@ async def _async_get_tts_audio( response = await self._client.synthesize_speech( request, - timeout=10, + timeout=30, retry=AsyncRetry(initial=0.1, maximum=2.0, multiplier=2.0), ) From 80c52ad8ea29db07dccf0b4c1f67edae178d60b2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 14 Jul 2025 11:25:22 +0200 Subject: [PATCH 49/96] Fix - only enable AlexaModeController if at least one mode is offered (#148614) --- homeassistant/components/alexa/entities.py | 23 ++- tests/components/alexa/test_entities.py | 165 +++++++++++++++++++++ 2 files changed, 183 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 7088b624e217dc..5f789813869f3f 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -505,8 +505,13 @@ def interfaces(self) -> Generator[AlexaCapability]: ): yield AlexaThermostatController(self.hass, self.entity) yield AlexaTemperatureSensor(self.hass, self.entity) - if self.entity.domain == water_heater.DOMAIN and ( - supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE + if ( + self.entity.domain == water_heater.DOMAIN + and ( + supported_features + & water_heater.WaterHeaterEntityFeature.OPERATION_MODE + ) + and self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST) ): yield AlexaModeController( self.entity, @@ -634,7 +639,9 @@ def interfaces(self) -> Generator[AlexaCapability]: self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" ) force_range_controller = False - if supported & fan.FanEntityFeature.PRESET_MODE: + if supported & fan.FanEntityFeature.PRESET_MODE and self.entity.attributes.get( + fan.ATTR_PRESET_MODES + ): yield AlexaModeController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}" ) @@ -672,7 +679,11 @@ def interfaces(self) -> Generator[AlexaCapability]: yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or [] - if activities and supported & remote.RemoteEntityFeature.ACTIVITY: + if ( + activities + and (supported & remote.RemoteEntityFeature.ACTIVITY) + and self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) + ): yield AlexaModeController( self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}" ) @@ -692,7 +703,9 @@ def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if supported & humidifier.HumidifierEntityFeature.MODES: + if ( + supported & humidifier.HumidifierEntityFeature.MODES + ) and self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES): yield AlexaModeController( self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}" ) diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 6998b2acc974ab..4d8d0dca67f93e 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -5,6 +5,7 @@ import pytest +from homeassistant.components import fan, humidifier, remote, water_heater from homeassistant.components.alexa import smart_home from homeassistant.const import EntityCategory, UnitOfTemperature, __version__ from homeassistant.core import HomeAssistant @@ -200,3 +201,167 @@ async def test_serialize_discovery_recovers( "Error serializing Alexa.PowerController discovery" f" for {hass.states.get('switch.bla')}" ) in caplog.text + + +@pytest.mark.parametrize( + ("domain", "state", "state_attributes", "mode_controller_exists"), + [ + ("switch", "on", {}, False), + ( + "fan", + "on", + { + "preset_modes": ["eco", "auto"], + "preset_mode": "eco", + "supported_features": fan.FanEntityFeature.PRESET_MODE.value, + }, + True, + ), + ( + "fan", + "on", + { + "preset_modes": ["eco", "auto"], + "preset_mode": None, + "supported_features": fan.FanEntityFeature.PRESET_MODE.value, + }, + True, + ), + ( + "fan", + "on", + { + "preset_modes": ["eco"], + "preset_mode": None, + "supported_features": fan.FanEntityFeature.PRESET_MODE.value, + }, + True, + ), + ( + "fan", + "on", + { + "preset_modes": [], + "preset_mode": None, + "supported_features": fan.FanEntityFeature.PRESET_MODE.value, + }, + False, + ), + ( + "humidifier", + "on", + { + "available_modes": ["auto", "manual"], + "mode": "auto", + "supported_features": humidifier.HumidifierEntityFeature.MODES.value, + }, + True, + ), + ( + "humidifier", + "on", + { + "available_modes": ["auto"], + "mode": None, + "supported_features": humidifier.HumidifierEntityFeature.MODES.value, + }, + True, + ), + ( + "humidifier", + "on", + { + "available_modes": [], + "mode": None, + "supported_features": humidifier.HumidifierEntityFeature.MODES.value, + }, + False, + ), + ( + "remote", + "on", + { + "activity_list": ["tv", "dvd"], + "current_activity": "tv", + "supported_features": remote.RemoteEntityFeature.ACTIVITY.value, + }, + True, + ), + ( + "remote", + "on", + { + "activity_list": ["tv"], + "current_activity": None, + "supported_features": remote.RemoteEntityFeature.ACTIVITY.value, + }, + True, + ), + ( + "remote", + "on", + { + "activity_list": [], + "current_activity": None, + "supported_features": remote.RemoteEntityFeature.ACTIVITY.value, + }, + False, + ), + ( + "water_heater", + "on", + { + "operation_list": ["on", "auto"], + "operation_mode": "auto", + "supported_features": water_heater.WaterHeaterEntityFeature.OPERATION_MODE.value, + }, + True, + ), + ( + "water_heater", + "on", + { + "operation_list": ["on"], + "operation_mode": None, + "supported_features": water_heater.WaterHeaterEntityFeature.OPERATION_MODE.value, + }, + True, + ), + ( + "water_heater", + "on", + { + "operation_list": [], + "operation_mode": None, + "supported_features": water_heater.WaterHeaterEntityFeature.OPERATION_MODE.value, + }, + False, + ), + ], +) +async def test_mode_controller_is_omitted_if_no_modes_are_set( + hass: HomeAssistant, + domain: str, + state: str, + state_attributes: dict[str, Any], + mode_controller_exists: bool, +) -> None: + """Test we do not generate an invalid discovery with AlexaModeController during serialize discovery. + + AlexModeControllers need at least 2 modes. If one mode is set, an extra mode will be added for compatibility. + If no modes are offered, the mode controller should be omitted to prevent schema validations. + """ + request = get_new_request("Alexa.Discovery", "Discover") + + hass.states.async_set( + f"{domain}.bla", state, {"friendly_name": "Boop Woz"} | state_attributes + ) + + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) + msg = msg["event"] + + interfaces = { + ifc["interface"] for ifc in msg["payload"]["endpoints"][0]["capabilities"] + } + + assert ("Alexa.ModeController" in interfaces) is mode_controller_exists From 649fbfc729a40f05a848e50043b2c13a64a6cace Mon Sep 17 00:00:00 2001 From: falconindy Date: Sat, 12 Jul 2025 14:46:03 -0400 Subject: [PATCH 50/96] snoo: use correct value for right safety clip binary sensor (#148647) --- homeassistant/components/snoo/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/snoo/binary_sensor.py b/homeassistant/components/snoo/binary_sensor.py index 3c91db5b86dc9b..c4eaddcc1fe34f 100644 --- a/homeassistant/components/snoo/binary_sensor.py +++ b/homeassistant/components/snoo/binary_sensor.py @@ -38,7 +38,7 @@ class SnooBinarySensorEntityDescription(BinarySensorEntityDescription): SnooBinarySensorEntityDescription( key="right_clip", translation_key="right_clip", - value_fn=lambda data: data.left_safety_clip, + value_fn=lambda data: data.right_safety_clip, device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ), From c802430066bbf61004c7ed7789ccf2947ecef7bf Mon Sep 17 00:00:00 2001 From: 0xEF <48224539+hexEF@users.noreply.github.com> Date: Sat, 12 Jul 2025 20:30:26 +0200 Subject: [PATCH 51/96] Bump nyt_games to 0.5.0 (#148654) --- .../components/nyt_games/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/nyt_games/fixtures/latest.json | 57 ++++++++++--------- .../nyt_games/fixtures/new_account.json | 25 ++++---- .../nyt_games/snapshots/test_sensor.ambr | 8 +-- 6 files changed, 51 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/nyt_games/manifest.json b/homeassistant/components/nyt_games/manifest.json index c32de754782208..db3ad6a85f11fa 100644 --- a/homeassistant/components/nyt_games/manifest.json +++ b/homeassistant/components/nyt_games/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nyt_games", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["nyt_games==0.4.4"] + "requirements": ["nyt_games==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 02520158cd41b5..3eb2312b0799b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1555,7 +1555,7 @@ numato-gpio==0.13.0 numpy==2.3.0 # homeassistant.components.nyt_games -nyt_games==0.4.4 +nyt_games==0.5.0 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 374fc8cc36b4d3..c7e94225cf0039 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1329,7 +1329,7 @@ numato-gpio==0.13.0 numpy==2.3.0 # homeassistant.components.nyt_games -nyt_games==0.4.4 +nyt_games==0.5.0 # homeassistant.components.google oauth2client==4.1.3 diff --git a/tests/components/nyt_games/fixtures/latest.json b/tests/components/nyt_games/fixtures/latest.json index 73a6f440fc054a..16601243052eb3 100644 --- a/tests/components/nyt_games/fixtures/latest.json +++ b/tests/components/nyt_games/fixtures/latest.json @@ -25,43 +25,46 @@ }, "wordle": { "legacyStats": { - "gamesPlayed": 70, - "gamesWon": 51, + "gamesPlayed": 1111, + "gamesWon": 1069, "guesses": { "1": 0, - "2": 1, - "3": 7, - "4": 11, - "5": 20, - "6": 12, - "fail": 19 + "2": 8, + "3": 83, + "4": 440, + "5": 372, + "6": 166, + "fail": 42 }, - "currentStreak": 1, - "maxStreak": 5, - "lastWonDayOffset": 1189, + "currentStreak": 229, + "maxStreak": 229, + "lastWonDayOffset": 1472, "hasPlayed": true, - "autoOptInTimestamp": 1708273168957, - "hasMadeStatsChoice": false, - "timestamp": 1726831978 + "autoOptInTimestamp": 1712205417018, + "hasMadeStatsChoice": true, + "timestamp": 1751255756 }, "calculatedStats": { - "gamesPlayed": 33, - "gamesWon": 26, + "currentStreak": 237, + "maxStreak": 241, + "lastWonPrintDate": "2025-07-08", + "lastCompletedPrintDate": "2025-07-08", + "hasPlayed": true + }, + "totalStats": { + "gamesWon": 1077, + "gamesPlayed": 1119, "guesses": { "1": 0, - "2": 1, - "3": 4, - "4": 7, - "5": 10, - "6": 4, - "fail": 7 + "2": 8, + "3": 83, + "4": 444, + "5": 376, + "6": 166, + "fail": 42 }, - "currentStreak": 1, - "maxStreak": 5, - "lastWonPrintDate": "2024-09-20", - "lastCompletedPrintDate": "2024-09-20", "hasPlayed": true, - "generation": 1 + "hasPlayedArchive": false } } } diff --git a/tests/components/nyt_games/fixtures/new_account.json b/tests/components/nyt_games/fixtures/new_account.json index ad4d8e2e4164ea..d35ce4cdebc5c9 100644 --- a/tests/components/nyt_games/fixtures/new_account.json +++ b/tests/components/nyt_games/fixtures/new_account.json @@ -7,14 +7,14 @@ "stats": { "wordle": { "legacyStats": { - "gamesPlayed": 1, - "gamesWon": 1, + "gamesPlayed": 0, + "gamesWon": 0, "guesses": { "1": 0, "2": 0, "3": 0, "4": 0, - "5": 1, + "5": 0, "6": 0, "fail": 0 }, @@ -27,23 +27,26 @@ "timestamp": 1727358123 }, "calculatedStats": { - "gamesPlayed": 0, - "gamesWon": 0, + "currentStreak": 0, + "maxStreak": 1, + "lastWonPrintDate": "", + "lastCompletedPrintDate": "", + "hasPlayed": false + }, + "totalStats": { + "gamesPlayed": 1, + "gamesWon": 1, "guesses": { "1": 0, "2": 0, "3": 0, "4": 0, - "5": 0, + "5": 1, "6": 0, "fail": 0 }, - "currentStreak": 0, - "maxStreak": 1, - "lastWonPrintDate": "", - "lastCompletedPrintDate": "", "hasPlayed": false, - "generation": 1 + "hasPlayedArchive": false } } } diff --git a/tests/components/nyt_games/snapshots/test_sensor.ambr b/tests/components/nyt_games/snapshots/test_sensor.ambr index 5a1aa384f0feb4..10fddcfa365001 100644 --- a/tests/components/nyt_games/snapshots/test_sensor.ambr +++ b/tests/components/nyt_games/snapshots/test_sensor.ambr @@ -473,7 +473,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1', + 'state': '237', }) # --- # name: test_all_entities[sensor.wordle_highest_streak-entry] @@ -529,7 +529,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '241', }) # --- # name: test_all_entities[sensor.wordle_played-entry] @@ -581,7 +581,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '70', + 'state': '1119', }) # --- # name: test_all_entities[sensor.wordle_won-entry] @@ -633,6 +633,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '51', + 'state': '1077', }) # --- From c4ddcd64c827ff61b6e0d7a82e2e47e4d8a58f4a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 14 Jul 2025 18:29:29 +1000 Subject: [PATCH 52/96] Fix Charge Cable binary sensor in Teslemetry (#148675) --- homeassistant/components/teslemetry/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index 439df76c83816a..6905cefdc30ebf 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -125,8 +125,8 @@ class TeslemetryBinarySensorEntityDescription(BinarySensorEntityDescription): key="charge_state_conn_charge_cable", polling=True, polling_value_fn=lambda x: x != "", - streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType( - lambda value: callback(value is not None and value != "Unknown") + streaming_listener=lambda vehicle, callback: vehicle.listen_DetailedChargeState( + lambda value: callback(None if value is None else value != "Disconnected") ), entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, From d4374dbcc77c14a20eadf7aa28afb93199f35fc0 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Mon, 14 Jul 2025 08:08:54 +0200 Subject: [PATCH 53/96] Bump PyViCare to 2.50.0 (#148679) --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index fed777e643557e..8e632e46efe52a 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "iot_class": "cloud_polling", "loggers": ["PyViCare"], - "requirements": ["PyViCare==2.44.0"] + "requirements": ["PyViCare==2.50.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3eb2312b0799b2..8361bf5cefd9be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.8.0 # homeassistant.components.vicare -PyViCare==2.44.0 +PyViCare==2.50.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7e94225cf0039..6cf669bb013b22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.8.0 # homeassistant.components.vicare -PyViCare==2.44.0 +PyViCare==2.50.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 From f7672985ed3bc0fa2c1cee5c135a66ced5d1b10d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 14 Jul 2025 11:26:37 +0200 Subject: [PATCH 54/96] Fix hide empty sections in mqtt subentry flows (#148692) --- homeassistant/components/mqtt/config_flow.py | 3 ++ tests/components/mqtt/test_config_flow.py | 51 +++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index ee451b5f81d24e..a3cf2d1d12fe82 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -2114,6 +2114,9 @@ def get_default(field_details: PlatformField) -> Any: if schema_section is None: data_schema.update(data_schema_element) continue + if not data_schema_element: + # Do not show empty sections + continue collapsed = ( not any( (default := data_schema_fields[str(option)].default) is vol.UNDEFINED diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 9386f1da32c688..77c740019398bd 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -3220,7 +3220,7 @@ async def test_subentry_configflow( "url": learn_more_url(component["platform"]), } - # Process entity details setep + # Process entity details step assert result["step_id"] == "entity_platform_config" # First test validators if set of test @@ -4212,3 +4212,52 @@ async def test_subentry_reconfigure_availablity( "payload_available": "1", "payload_not_available": "0", } + + +async def test_subentry_configflow_section_feature( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test the subentry ConfigFlow sections are hidden when they have no configurable options.""" + await mqtt_mock_entry() + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + + result = await hass.config_entries.subentries.async_init( + (config_entry.entry_id, "device"), + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "device" + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={"name": "Bla", "mqtt_settings": {"qos": 1}}, + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={"platform": "fan"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["description_placeholders"] == { + "mqtt_device": "Bla", + "platform": "fan", + "entity": "Bla", + "url": learn_more_url("fan"), + } + + # Process entity details step + assert result["step_id"] == "entity_platform_config" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={"fan_feature_speed": True}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + assert result["step_id"] == "mqtt_platform_config" + + # Check mqtt platform config flow sections from data schema + data_schema = result["data_schema"].schema + assert "fan_speed_settings" in data_schema + assert "fan_preset_mode_settings" not in data_schema From 80384b89a5b3b4f1bc48e471d660f298ae507ea3 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 14 Jul 2025 01:15:50 +0300 Subject: [PATCH 55/96] Bump aioshelly to 13.7.2 (#148706) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 1db8dbf55c6b7f..08c9163bb3b020 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "silver", - "requirements": ["aioshelly==13.7.1"], + "requirements": ["aioshelly==13.7.2"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 8361bf5cefd9be..c3b3239101ebc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -381,7 +381,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.7.1 +aioshelly==13.7.2 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6cf669bb013b22..b34163d2ee85df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -363,7 +363,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.7.1 +aioshelly==13.7.2 # homeassistant.components.skybell aioskybell==22.7.0 From 691a0ca065aba13f2737cc66d0427087f056a52d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 14 Jul 2025 01:18:35 +0300 Subject: [PATCH 56/96] Bump aioamazondevices to 3.2.10 (#148709) --- homeassistant/components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 34fdd1448a5364..0cb99ba090ee0c 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.2.8"] + "requirements": ["aioamazondevices==3.2.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index c3b3239101ebc0..c7d125c0700234 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.2.8 +aioamazondevices==3.2.10 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b34163d2ee85df..a388e6dbf8781a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.2.8 +aioamazondevices==3.2.10 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 87af9fc8baab7a248fcfaf9fd89955e7f6b941cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Jul 2025 10:30:35 +0000 Subject: [PATCH 57/96] Bump version to 2025.7.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 258a9c9a48f2f2..9dd3c3480f953e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 7 -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 17fb74f452c3e3..47c38246f29caa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.7.1" +version = "2025.7.2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From c6d0aad3d33049cd02dea204f6c3b021f52a42c5 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Tue, 15 Jul 2025 12:15:19 +0200 Subject: [PATCH 58/96] Handle connection issues after websocket reconnected in homematicip_cloud (#147731) --- .../components/homematicip_cloud/hap.py | 63 ++++++++++++------- .../homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homematicip_cloud/test_device.py | 11 +++- .../components/homematicip_cloud/test_hap.py | 61 ++++++++++++++++-- 6 files changed, 107 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index c42ebff200d222..d66594da390df1 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -113,9 +113,7 @@ def __init__( self._ws_close_requested = False self._ws_connection_closed = asyncio.Event() - self._retry_task: asyncio.Task | None = None - self._tries = 0 - self._accesspoint_connected = True + self._get_state_task: asyncio.Task | None = None self.hmip_device_by_entity_id: dict[str, Any] = {} self.reset_connection_listener: Callable | None = None @@ -161,17 +159,8 @@ def async_update(self, *args, **kwargs) -> None: """ if not self.home.connected: _LOGGER.error("HMIP access point has lost connection with the cloud") - self._accesspoint_connected = False + self._ws_connection_closed.set() self.set_all_to_unavailable() - elif not self._accesspoint_connected: - # Now the HOME_CHANGED event has fired indicating the access - # point has reconnected to the cloud again. - # Explicitly getting an update as entity states might have - # changed during access point disconnect.""" - - job = self.hass.async_create_task(self.get_state()) - job.add_done_callback(self.get_state_finished) - self._accesspoint_connected = True @callback def async_create_entity(self, *args, **kwargs) -> None: @@ -185,20 +174,43 @@ async def async_create_entity_lazy(self, is_device=True) -> None: await asyncio.sleep(30) await self.hass.config_entries.async_reload(self.config_entry.entry_id) + async def _try_get_state(self) -> None: + """Call get_state in a loop until no error occurs, using exponential backoff on error.""" + + # Wait until WebSocket connection is established. + while not self.home.websocket_is_connected(): + await asyncio.sleep(2) + + delay = 8 + max_delay = 1500 + while True: + try: + await self.get_state() + break + except HmipConnectionError as err: + _LOGGER.warning( + "Get_state failed, retrying in %s seconds: %s", delay, err + ) + await asyncio.sleep(delay) + delay = min(delay * 2, max_delay) + async def get_state(self) -> None: """Update HMIP state and tell Home Assistant.""" await self.home.get_current_state_async() self.update_all() def get_state_finished(self, future) -> None: - """Execute when get_state coroutine has finished.""" + """Execute when try_get_state coroutine has finished.""" try: future.result() - except HmipConnectionError: - # Somehow connection could not recover. Will disconnect and - # so reconnect loop is taking over. - _LOGGER.error("Updating state after HMIP access point reconnect failed") - self.hass.async_create_task(self.home.disable_events()) + except Exception as err: # noqa: BLE001 + _LOGGER.error( + "Error updating state after HMIP access point reconnect: %s", err + ) + else: + _LOGGER.info( + "Updating state after HMIP access point reconnect finished successfully", + ) def set_all_to_unavailable(self) -> None: """Set all devices to unavailable and tell Home Assistant.""" @@ -222,8 +234,8 @@ async def async_connect(self, home: AsyncHome) -> None: async def async_reset(self) -> bool: """Close the websocket connection.""" self._ws_close_requested = True - if self._retry_task is not None: - self._retry_task.cancel() + if self._get_state_task is not None: + self._get_state_task.cancel() await self.home.disable_events_async() _LOGGER.debug("Closed connection to HomematicIP cloud server") await self.hass.config_entries.async_unload_platforms( @@ -247,7 +259,9 @@ async def ws_connected_handler(self) -> None: """Handle websocket connected.""" _LOGGER.info("Websocket connection to HomematicIP Cloud established") if self._ws_connection_closed.is_set(): - await self.get_state() + self._get_state_task = self.hass.async_create_task(self._try_get_state()) + self._get_state_task.add_done_callback(self.get_state_finished) + self._ws_connection_closed.clear() async def ws_disconnected_handler(self) -> None: @@ -256,11 +270,12 @@ async def ws_disconnected_handler(self) -> None: self._ws_connection_closed.set() async def ws_reconnected_handler(self, reason: str) -> None: - """Handle websocket reconnection.""" + """Handle websocket reconnection. Is called when Websocket tries to reconnect.""" _LOGGER.info( - "Websocket connection to HomematicIP Cloud re-established due to reason: %s", + "Websocket connection to HomematicIP Cloud trying to reconnect due to reason: %s", reason, ) + self._ws_connection_closed.set() async def get_hap( diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index d5af285987377d..036ffa286a3701 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "iot_class": "cloud_push", "loggers": ["homematicip"], - "requirements": ["homematicip==2.0.6"] + "requirements": ["homematicip==2.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index c7d125c0700234..4d9efd17d2394f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ home-assistant-frontend==20250702.2 home-assistant-intents==2025.6.23 # homeassistant.components.homematicip_cloud -homematicip==2.0.6 +homematicip==2.0.7 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a388e6dbf8781a..dd15dfca16aea6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ home-assistant-frontend==20250702.2 home-assistant-intents==2025.6.23 # homeassistant.components.homematicip_cloud -homematicip==2.0.6 +homematicip==2.0.7 # homeassistant.components.remember_the_milk httplib2==0.20.4 diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index abd0e18b3686ed..48ad8806d0c7df 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -195,9 +195,14 @@ async def test_hap_reconnected( ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_UNAVAILABLE - mock_hap._accesspoint_connected = False - await async_manipulate_test_data(hass, mock_hap.home, "connected", True) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome.websocket_is_connected", + return_value=True, + ): + await async_manipulate_test_data(hass, mock_hap.home, "connected", True) + await mock_hap.ws_connected_handler() + await hass.async_block_till_done() + ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index ae094f7ddeda41..69078beafafe10 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,6 +1,6 @@ """Test HomematicIP Cloud accesspoint.""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from homematicip.auth import Auth from homematicip.connection.connection_context import ConnectionContext @@ -242,7 +242,14 @@ async def test_get_state_after_disconnect( hap = HomematicipHAP(hass, hmip_config_entry) assert hap - with patch.object(hap, "get_state") as mock_get_state: + simple_mock_home = AsyncMock(spec=AsyncHome, autospec=True) + hap.home = simple_mock_home + hap.home.websocket_is_connected = Mock(side_effect=[False, True]) + + with ( + patch("asyncio.sleep", new=AsyncMock()) as mock_sleep, + patch.object(hap, "get_state") as mock_get_state, + ): assert not hap._ws_connection_closed.is_set() await hap.ws_connected_handler() @@ -250,8 +257,54 @@ async def test_get_state_after_disconnect( await hap.ws_disconnected_handler() assert hap._ws_connection_closed.is_set() - await hap.ws_connected_handler() - mock_get_state.assert_called_once() + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome.websocket_is_connected", + return_value=True, + ): + await hap.ws_connected_handler() + mock_get_state.assert_called_once() + + assert not hap._ws_connection_closed.is_set() + hap.home.websocket_is_connected.assert_called() + mock_sleep.assert_awaited_with(2) + + +async def test_try_get_state_exponential_backoff() -> None: + """Test _try_get_state waits for websocket connection.""" + + # Arrange: Create instance and mock home + hap = HomematicipHAP(MagicMock(), MagicMock()) + hap.home = MagicMock() + hap.home.websocket_is_connected = Mock(return_value=True) + + hap.get_state = AsyncMock( + side_effect=[HmipConnectionError, HmipConnectionError, True] + ) + + with patch("asyncio.sleep", new=AsyncMock()) as mock_sleep: + await hap._try_get_state() + + assert mock_sleep.mock_calls[0].args[0] == 8 + assert mock_sleep.mock_calls[1].args[0] == 16 + assert hap.get_state.call_count == 3 + + +async def test_try_get_state_handle_exception() -> None: + """Test _try_get_state handles exceptions.""" + # Arrange: Create instance and mock home + hap = HomematicipHAP(MagicMock(), MagicMock()) + hap.home = MagicMock() + + expected_exception = Exception("Connection error") + future = AsyncMock() + future.result = Mock(side_effect=expected_exception) + + with patch("homeassistant.components.homematicip_cloud.hap._LOGGER") as mock_logger: + hap.get_state_finished(future) + + mock_logger.error.assert_called_once_with( + "Error updating state after HMIP access point reconnect: %s", expected_exception + ) async def test_async_connect( From caf049200933178fa8c6c134ea976026700c63a6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 14 Jul 2025 22:40:46 +0200 Subject: [PATCH 59/96] Fix Shelly `n_current` sensor removal condition (#148740) --- homeassistant/components/shelly/sensor.py | 4 +- tests/components/shelly/fixtures/pro_3em.json | 2 +- .../shelly/snapshots/test_devices.ambr | 56 +++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 3a6f5f221c52bd..cefcbb86a98ead 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -868,8 +868,8 @@ def __init__( native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, - available=lambda status: (status and status["n_current"]) is not None, - removal_condition=lambda _config, status, _key: "n_current" not in status, + removal_condition=lambda _config, status, key: status[key].get("n_current") + is None, entity_registry_enabled_default=False, ), "total_current": RpcSensorDescription( diff --git a/tests/components/shelly/fixtures/pro_3em.json b/tests/components/shelly/fixtures/pro_3em.json index 93351e9bc65ab0..4895766cc498c8 100644 --- a/tests/components/shelly/fixtures/pro_3em.json +++ b/tests/components/shelly/fixtures/pro_3em.json @@ -151,7 +151,7 @@ "c_pf": 0.72, "c_voltage": 230.2, "id": 0, - "n_current": null, + "n_current": 3.124, "total_act_power": 2413.825, "total_aprt_power": 2525.779, "total_current": 11.116, diff --git a/tests/components/shelly/snapshots/test_devices.ambr b/tests/components/shelly/snapshots/test_devices.ambr index 0b8ec71771bd6a..9dcda321057e28 100644 --- a/tests/components/shelly/snapshots/test_devices.ambr +++ b/tests/components/shelly/snapshots/test_devices.ambr @@ -4303,6 +4303,62 @@ 'state': '230.2', }) # --- +# name: test_shelly_pro_3em[sensor.test_name_phase_n_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_name_phase_n_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase N current', + 'platform': 'shelly', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-em:0-n_current', + 'unit_of_measurement': , + }) +# --- +# name: test_shelly_pro_3em[sensor.test_name_phase_n_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Test name Phase N current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_name_phase_n_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.124', + }) +# --- # name: test_shelly_pro_3em[sensor.test_name_rssi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 56e0aa103dbb3ee41f5b8146dde0be7512f459ca Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 14 Jul 2025 21:18:12 +0200 Subject: [PATCH 60/96] Bump pySmartThings to 3.2.8 (#148761) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 2c4974a65672dc..35354570f232b6 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.7"] + "requirements": ["pysmartthings==3.2.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4d9efd17d2394f..e69675779db534 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2348,7 +2348,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.0 # homeassistant.components.smartthings -pysmartthings==3.2.7 +pysmartthings==3.2.8 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd15dfca16aea6..533573e2a3679a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1951,7 +1951,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.0 # homeassistant.components.smartthings -pysmartthings==3.2.7 +pysmartthings==3.2.8 # homeassistant.components.smarty pysmarty2==0.10.2 From 69fdc1d26930484895a79d3302a07d894ff7201b Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 15 Jul 2025 17:21:00 +1000 Subject: [PATCH 61/96] Bump Tesla Fleet API to 1.2.2 (#148776) --- homeassistant/components/tesla_fleet/manifest.json | 2 +- homeassistant/components/teslemetry/manifest.json | 2 +- homeassistant/components/tessie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tesla_fleet/manifest.json b/homeassistant/components/tesla_fleet/manifest.json index 4c92e0bd222db3..cf86fbeb4f96d5 100644 --- a/homeassistant/components/tesla_fleet/manifest.json +++ b/homeassistant/components/tesla_fleet/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tesla_fleet", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.2.0"] + "requirements": ["tesla-fleet-api==1.2.2"] } diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index f58783e04a4ba8..d12cf278d59062 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.2.0", "teslemetry-stream==0.7.9"] + "requirements": ["tesla-fleet-api==1.2.2", "teslemetry-stream==0.7.9"] } diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index c0cbc2ea43109f..26f26990d58dfd 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie", "tesla-fleet-api"], - "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.0"] + "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e69675779db534..61e344b9f3e7d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2904,7 +2904,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==1.2.0 +tesla-fleet-api==1.2.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 533573e2a3679a..554905e48fd56b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2390,7 +2390,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==1.2.0 +tesla-fleet-api==1.2.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 From 9e4b8df344c2f854904689c8812056829c4a2e96 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 15 Jul 2025 17:38:19 +0200 Subject: [PATCH 62/96] Use ffmpeg for generic cameras in go2rtc (#148818) --- homeassistant/components/go2rtc/__init__.py | 5 ++++ tests/components/go2rtc/test_init.py | 29 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index 4e15b93330c1f8..8d3e988dd14f0b 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -306,6 +306,11 @@ async def _update_stream_source(self, camera: Camera) -> None: await self.teardown() raise HomeAssistantError("Camera has no stream source") + if camera.platform.platform_name == "generic": + # This is a workaround to use ffmpeg for generic cameras + # A proper fix will be added in the future together with supporting multiple streams per camera + stream_source = "ffmpeg:" + stream_source + if not self.async_is_supported(stream_source): await self.teardown() raise HomeAssistantError("Stream source is not supported by go2rtc") diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index 2abdf724f616c6..dcbcb629d11ab0 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -670,3 +670,32 @@ async def test_async_get_image( HomeAssistantError, match="Stream source is not supported by go2rtc" ): await async_get_image(hass, camera.entity_id) + + +@pytest.mark.usefixtures("init_integration") +async def test_generic_workaround( + hass: HomeAssistant, + init_test_integration: MockCamera, + rest_client: AsyncMock, +) -> None: + """Test workaround for generic integration cameras.""" + camera = init_test_integration + assert isinstance(camera._webrtc_provider, WebRTCProvider) + + image_bytes = load_fixture_bytes("snapshot.jpg", DOMAIN) + + rest_client.get_jpeg_snapshot.return_value = image_bytes + camera.set_stream_source("https://my_stream_url.m3u8") + + with patch.object(camera.platform, "platform_name", "generic"): + image = await async_get_image(hass, camera.entity_id) + assert image.content == image_bytes + + rest_client.streams.add.assert_called_once_with( + camera.entity_id, + [ + "ffmpeg:https://my_stream_url.m3u8", + f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + f"ffmpeg:{camera.entity_id}#video=mjpeg", + ], + ) From 8e0a89dc2f295cb24ff80a5440817695405a66b2 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 16 Jul 2025 02:09:24 -0400 Subject: [PATCH 63/96] Add guard to prevent exception in Sonos Favorites (#148854) --- homeassistant/components/sonos/favorites.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index f8b3dbbe492e5c..f3471d01da1807 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -72,7 +72,7 @@ async def async_process_event( """Process the event payload in an async lock and update entities.""" event_id = event.variables["favorites_update_id"] container_ids = event.variables["container_update_i_ds"] - if not (match := re.search(r"FV:2,(\d+)", container_ids)): + if not container_ids or not (match := re.search(r"FV:2,(\d+)", container_ids)): return container_id = int(match.groups()[0]) From 1644484c92abb269b469d3b09f31e399065aa24c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 16 Jul 2025 16:16:36 +1000 Subject: [PATCH 64/96] Fix button platform parent class in Teslemetry (#148863) --- homeassistant/components/teslemetry/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/button.py b/homeassistant/components/teslemetry/button.py index cf1d6157ec141b..12772b894b652d 100644 --- a/homeassistant/components/teslemetry/button.py +++ b/homeassistant/components/teslemetry/button.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import TeslemetryConfigEntry -from .entity import TeslemetryVehiclePollingEntity +from .entity import TeslemetryVehicleStreamEntity from .helpers import handle_command, handle_vehicle_command from .models import TeslemetryVehicleData @@ -74,7 +74,7 @@ async def async_setup_entry( ) -class TeslemetryButtonEntity(TeslemetryVehiclePollingEntity, ButtonEntity): +class TeslemetryButtonEntity(TeslemetryVehicleStreamEntity, ButtonEntity): """Base class for Teslemetry buttons.""" api: Vehicle From 11a2c73e8ace4c1102a207f40a10e166e8e8dd28 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:18:14 +0200 Subject: [PATCH 65/96] Bump pyenphase to 2.2.2 (#148870) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 278045001fc1f4..320179bf2dff2d 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["pyenphase"], "quality_scale": "platinum", - "requirements": ["pyenphase==2.2.1"], + "requirements": ["pyenphase==2.2.2"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 61e344b9f3e7d3..6451b7116639bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1962,7 +1962,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==2.2.1 +pyenphase==2.2.2 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 554905e48fd56b..70748d6c5fe7ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1637,7 +1637,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==2.2.1 +pyenphase==2.2.2 # homeassistant.components.everlights pyeverlights==0.1.0 From 7a3eb53453e1f5f7af9a76873a03c5d5e4353aca Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Jul 2025 16:11:15 +0200 Subject: [PATCH 66/96] Bump gios to version 6.1.1 (#148414) --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index ba87890de03b72..1782320a357cad 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["dacite", "gios"], - "requirements": ["gios==6.1.0"] + "requirements": ["gios==6.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6451b7116639bd..ca5075fd9951f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1020,7 +1020,7 @@ georss-qld-bushfire-alert-client==0.8 getmac==0.9.5 # homeassistant.components.gios -gios==6.1.0 +gios==6.1.1 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70748d6c5fe7ff..2e0f0a0d96cfd1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -890,7 +890,7 @@ georss-qld-bushfire-alert-client==0.8 getmac==0.9.5 # homeassistant.components.gios -gios==6.1.0 +gios==6.1.1 # homeassistant.components.glances glances-api==0.8.0 From b6edcc942283b15a42b4a78a6e223ea4f6c4420a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 16 Jul 2025 15:49:02 +0200 Subject: [PATCH 67/96] Bump `gios` to version 6.1.2 (#148884) --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 1782320a357cad..8c6765ece895b5 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["dacite", "gios"], - "requirements": ["gios==6.1.1"] + "requirements": ["gios==6.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index ca5075fd9951f3..967535ca0b7e12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1020,7 +1020,7 @@ georss-qld-bushfire-alert-client==0.8 getmac==0.9.5 # homeassistant.components.gios -gios==6.1.1 +gios==6.1.2 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e0f0a0d96cfd1..337ed2e849dd29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -890,7 +890,7 @@ georss-qld-bushfire-alert-client==0.8 getmac==0.9.5 # homeassistant.components.gios -gios==6.1.1 +gios==6.1.2 # homeassistant.components.glances glances-api==0.8.0 From 5656b4c20d4fecc63c7a6c42a8071fab56a06760 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 17 Jul 2025 21:19:47 +0200 Subject: [PATCH 68/96] Bump async-upnp-client to 0.45.0 (#148961) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 119d1d31d52161..eac8ddcf713a89 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"], + "requirements": ["async-upnp-client==0.45.0", "getmac==0.9.5"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 0289d5100d6c16..4a73bf779e04f0 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -7,7 +7,7 @@ "dependencies": ["ssdp"], "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", - "requirements": ["async-upnp-client==0.44.0"], + "requirements": ["async-upnp-client==0.45.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index a2ab8e6e466ac0..1b927757a3964b 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -40,7 +40,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.7.2", "wakeonlan==3.1.0", - "async-upnp-client==0.44.0" + "async-upnp-client==0.45.0" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 93943b0a9eae2e..2471e45b4e06af 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.44.0"] + "requirements": ["async-upnp-client==0.45.0"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 62ee4ede7d97cc..825c5774c1df50 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"], + "requirements": ["async-upnp-client==0.45.0", "getmac==0.9.5"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 07970cb25cae0f..d65ebb3a25a254 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -16,7 +16,7 @@ }, "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], - "requirements": ["yeelight==0.7.16", "async-upnp-client==0.44.0"], + "requirements": ["yeelight==0.7.16", "async-upnp-client==0.45.0"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 01bf8e248857cf..98124be5c6494d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ aiozoneinfo==0.2.3 annotatedyaml==0.4.5 astral==2.2 async-interrupt==1.2.2 -async-upnp-client==0.44.0 +async-upnp-client==0.45.0 atomicwrites-homeassistant==1.4.1 attrs==25.3.0 audioop-lts==0.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 967535ca0b7e12..4b3048742a9bb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -527,7 +527,7 @@ asmog==0.0.6 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.44.0 +async-upnp-client==0.45.0 # homeassistant.components.arve asyncarve==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 337ed2e849dd29..6e0659ce614e8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -491,7 +491,7 @@ arcam-fmj==1.8.1 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.44.0 +async-upnp-client==0.45.0 # homeassistant.components.arve asyncarve==0.1.1 From 8fdc50a29f6ac601b3d57c68a0a9e63840280898 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 17 Jul 2025 23:14:19 +0200 Subject: [PATCH 69/96] Pass Syncthru entry to coordinator (#148974) --- homeassistant/components/syncthru/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/syncthru/coordinator.py b/homeassistant/components/syncthru/coordinator.py index 0b96b354436541..27239a5a520b0f 100644 --- a/homeassistant/components/syncthru/coordinator.py +++ b/homeassistant/components/syncthru/coordinator.py @@ -28,6 +28,7 @@ def __init__(self, hass: HomeAssistant, entry: SyncThruConfigEntry) -> None: hass, _LOGGER, name=DOMAIN, + config_entry=entry, update_interval=timedelta(seconds=30), ) self.syncthru = SyncThru( From 68889e1790b09fb180f2b9c825ccae9b68efaa92 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 18 Jul 2025 13:15:55 +0200 Subject: [PATCH 70/96] Update frontend to 20250702.3 (#148994) --- 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 a7582ebc5e25aa..791acf8a39cf74 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==20250702.2"] + "requirements": ["home-assistant-frontend==20250702.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98124be5c6494d..e783e8e074381d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.49.0 hass-nabucasa==0.106.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250702.2 +home-assistant-frontend==20250702.3 home-assistant-intents==2025.6.23 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4b3048742a9bb9..c65bc509fa05cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1168,7 +1168,7 @@ hole==0.8.0 holidays==0.75 # homeassistant.components.frontend -home-assistant-frontend==20250702.2 +home-assistant-frontend==20250702.3 # homeassistant.components.conversation home-assistant-intents==2025.6.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e0659ce614e8b..560071f9644925 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,7 +1017,7 @@ hole==0.8.0 holidays==0.75 # homeassistant.components.frontend -home-assistant-frontend==20250702.2 +home-assistant-frontend==20250702.3 # homeassistant.components.conversation home-assistant-intents==2025.6.23 From d57c5ffa8fb4e44a377d55cdb5660ddb4cfe03fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 00:48:09 -1000 Subject: [PATCH 71/96] Bump PySwitchbot to 0.68.2 (#148996) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 5ef7eec9976bd5..22168c21f97f12 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -41,5 +41,5 @@ "iot_class": "local_push", "loggers": ["switchbot"], "quality_scale": "gold", - "requirements": ["PySwitchbot==0.68.1"] + "requirements": ["PySwitchbot==0.68.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c65bc509fa05cb..af8161e4636c08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -84,7 +84,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.68.1 +PySwitchbot==0.68.2 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 560071f9644925..e0b95a9c4cef71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.68.1 +PySwitchbot==0.68.2 # homeassistant.components.syncthru PySyncThru==0.8.0 From c6bb26be89106bd1990cb9a0411c60b841e90ce6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 18 Jul 2025 18:57:03 +0200 Subject: [PATCH 72/96] Ignore MQTT sensor unit of measurement if it is an empty string (#149006) --- homeassistant/components/mqtt/sensor.py | 6 ++++ tests/components/mqtt/test_sensor.py | 39 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 783a0b30b14e27..83679894d71cfb 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -98,6 +98,12 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT f"together with state class `{state_class}`" ) + unit_of_measurement: str | None + if ( + unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT) + ) is not None and not unit_of_measurement.strip(): + config.pop(CONF_UNIT_OF_MEASUREMENT) + # Only allow `options` to be set for `enum` sensors # to limit the possible sensor values if (options := config.get(CONF_OPTIONS)) is not None: diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 997c014cd13fef..16f0c9f22bc11f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -924,6 +924,30 @@ async def test_invalid_unit_of_measurement( "device_class": None, "unit_of_measurement": None, }, + { + "name": "Test 4", + "state_topic": "test-topic", + "device_class": "ph", + "unit_of_measurement": "", + }, + { + "name": "Test 5", + "state_topic": "test-topic", + "device_class": "ph", + "unit_of_measurement": " ", + }, + { + "name": "Test 6", + "state_topic": "test-topic", + "device_class": None, + "unit_of_measurement": "", + }, + { + "name": "Test 7", + "state_topic": "test-topic", + "device_class": None, + "unit_of_measurement": " ", + }, ] } } @@ -936,10 +960,25 @@ async def test_valid_device_class_and_uom( await mqtt_mock_entry() state = hass.states.get("sensor.test_1") + assert state is not None assert state.attributes["device_class"] == "temperature" state = hass.states.get("sensor.test_2") + assert state is not None assert "device_class" not in state.attributes state = hass.states.get("sensor.test_3") + assert state is not None + assert "device_class" not in state.attributes + state = hass.states.get("sensor.test_4") + assert state is not None + assert state.attributes["device_class"] == "ph" + state = hass.states.get("sensor.test_5") + assert state is not None + assert state.attributes["device_class"] == "ph" + state = hass.states.get("sensor.test_6") + assert state is not None + assert "device_class" not in state.attributes + state = hass.states.get("sensor.test_7") + assert state is not None assert "device_class" not in state.attributes From 190c98f5a8252cf9b6164e15f99762b1fbeab329 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 18 Jul 2025 16:35:38 +0300 Subject: [PATCH 73/96] Bump aioamazondevices to 3.5.0 (#149011) --- homeassistant/components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 0cb99ba090ee0c..6904f8000ce88c 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.2.10"] + "requirements": ["aioamazondevices==3.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index af8161e4636c08..c9f708ad9abae9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.2.10 +aioamazondevices==3.5.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0b95a9c4cef71..2e0ad947c38acc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.2.10 +aioamazondevices==3.5.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 0675e34c6254fdb2d3e528b1707c432e8da66173 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Jul 2025 17:05:52 +0000 Subject: [PATCH 74/96] Bump version to 2025.7.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 9dd3c3480f953e..62e6d49befd225 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 7 -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 47c38246f29caa..c010ecb725488e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.7.2" +version = "2025.7.3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 5b08724d81a74778d27fae6d5a9839f3789b49ed Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Fri, 11 Jul 2025 23:45:57 +0200 Subject: [PATCH 75/96] Keep entities of dead Z-Wave devices available (#148611) --- homeassistant/components/zwave_js/entity.py | 22 +---------- homeassistant/components/zwave_js/update.py | 19 ++++------ tests/components/zwave_js/test_init.py | 42 ++++++++++++++++++++- tests/components/zwave_js/test_lock.py | 5 ++- tests/components/zwave_js/test_update.py | 14 +------ 5 files changed, 54 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index d1ab9009308979..08a587d8d20cc1 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -5,7 +5,6 @@ from collections.abc import Sequence from typing import Any -from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import ( @@ -27,8 +26,6 @@ from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id EVENT_VALUE_REMOVED = "value removed" -EVENT_DEAD = "dead" -EVENT_ALIVE = "alive" class ZWaveBaseEntity(Entity): @@ -141,11 +138,6 @@ async def async_added_to_hass(self) -> None: ) ) - for status_event in (EVENT_ALIVE, EVENT_DEAD): - self.async_on_remove( - self.info.node.on(status_event, self._node_status_alive_or_dead) - ) - self.async_on_remove( async_dispatcher_connect( self.hass, @@ -211,19 +203,7 @@ def generate_name( @property def available(self) -> bool: """Return entity availability.""" - return ( - self.driver.client.connected - and bool(self.info.node.ready) - and self.info.node.status != NodeStatus.DEAD - ) - - @callback - def _node_status_alive_or_dead(self, event_data: dict) -> None: - """Call when node status changes to alive or dead. - - Should not be overridden by subclasses. - """ - self.async_write_ha_state() + return self.driver.client.connected and bool(self.info.node.ready) @callback def _value_changed(self, event_data: dict) -> None: diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 985c4a86813008..bf32754f50df6d 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -200,18 +200,13 @@ async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None ) return - # If device is asleep/dead, wait for it to wake up/become alive before - # attempting an update - for status, event_name in ( - (NodeStatus.ASLEEP, "wake up"), - (NodeStatus.DEAD, "alive"), - ): - if self.node.status == status: - if not self._status_unsub: - self._status_unsub = self.node.once( - event_name, self._update_on_status_change - ) - return + # If device is asleep, wait for it to wake up before attempting an update + if self.node.status == NodeStatus.ASLEEP: + if not self._status_unsub: + self._status_unsub = self.node.once( + "wake up", self._update_on_status_change + ) + return try: # Retrieve all firmware updates including non-stable ones but filter diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 4350d7f76491e1..324a0f149419f9 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -37,7 +37,11 @@ ) from homeassistant.setup import async_setup_component -from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY +from .common import ( + AIR_TEMPERATURE_SENSOR, + BULB_6_MULTI_COLOR_LIGHT_ENTITY, + EATON_RF9640_ENTITY, +) from tests.common import ( MockConfigEntry, @@ -2168,3 +2172,39 @@ async def test_factory_reset_node( assert len(notifications) == 1 assert list(notifications)[0] == msg_id assert "network with the home ID `3245146787`" in notifications[msg_id]["message"] + + +async def test_entity_available_when_node_dead( + hass: HomeAssistant, client, bulb_6_multi_color, integration +) -> None: + """Test that entities remain available even when the node is dead.""" + + node = bulb_6_multi_color + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + + assert state + assert state.state != STATE_UNAVAILABLE + + # Send dead event to the node + event = Event( + "dead", data={"source": "node", "event": "dead", "nodeId": node.node_id} + ) + node.receive_event(event) + await hass.async_block_till_done() + + # Entity should remain available even though the node is dead + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + assert state + assert state.state != STATE_UNAVAILABLE + + # Send alive event to bring the node back + event = Event( + "alive", data={"source": "node", "event": "alive", "nodeId": node.node_id} + ) + node.receive_event(event) + await hass.async_block_till_done() + + # Entity should still be available + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + assert state + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 1011026ac6813d..9e36810872f4e1 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -28,7 +28,7 @@ SERVICE_SET_LOCK_CONFIGURATION, SERVICE_SET_LOCK_USERCODE, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -295,7 +295,8 @@ async def test_door_lock( assert node.status == NodeStatus.DEAD state = hass.states.get(SCHLAGE_BE469_LOCK_ENTITY) assert state - assert state.state == STATE_UNAVAILABLE + # The state should still be locked, even if the node is dead + assert state.state == LockState.LOCKED async def test_only_one_lock( diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index fc225d529a6962..17f154f4f78657 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -277,7 +277,7 @@ async def test_update_entity_dead( zen_31, integration, ) -> None: - """Test update occurs when device is dead after it becomes alive.""" + """Test update occurs even when device is dead.""" event = Event( "dead", data={"source": "node", "event": "dead", "nodeId": zen_31.node_id}, @@ -290,17 +290,7 @@ async def test_update_entity_dead( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - # Because node is asleep we shouldn't attempt to check for firmware updates - assert len(client.async_send_command.call_args_list) == 0 - - event = Event( - "alive", - data={"source": "node", "event": "alive", "nodeId": zen_31.node_id}, - ) - zen_31.receive_event(event) - await hass.async_block_till_done() - - # Now that the node is up we can check for updates + # Checking for firmware updates should proceed even for dead nodes assert len(client.async_send_command.call_args_list) > 0 args = client.async_send_command.call_args_list[0][0][0] From 254ccca4e511e9e5aa8a71c0445fce285cf8cbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Wed, 23 Jul 2025 17:05:35 +0200 Subject: [PATCH 76/96] Fix warning about failure to get action during setup phase (#148923) --- homeassistant/components/wmspro/button.py | 2 +- homeassistant/components/wmspro/cover.py | 4 ++-- homeassistant/components/wmspro/light.py | 4 ++-- homeassistant/components/wmspro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wmspro/button.py b/homeassistant/components/wmspro/button.py index f1ab0489b8628e..1b2772a9c80ade 100644 --- a/homeassistant/components/wmspro/button.py +++ b/homeassistant/components/wmspro/button.py @@ -23,7 +23,7 @@ async def async_setup_entry( entities: list[WebControlProGenericEntity] = [ WebControlProIdentifyButton(config_entry.entry_id, dest) for dest in hub.dests.values() - if dest.action(WMS_WebControl_pro_API_actionDescription.Identify) + if dest.hasAction(WMS_WebControl_pro_API_actionDescription.Identify) ] async_add_entities(entities) diff --git a/homeassistant/components/wmspro/cover.py b/homeassistant/components/wmspro/cover.py index b6f100280adfd2..e7255d478cb296 100644 --- a/homeassistant/components/wmspro/cover.py +++ b/homeassistant/components/wmspro/cover.py @@ -32,9 +32,9 @@ async def async_setup_entry( entities: list[WebControlProGenericEntity] = [] for dest in hub.dests.values(): - if dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive): + if dest.hasAction(WMS_WebControl_pro_API_actionDescription.AwningDrive): entities.append(WebControlProAwning(config_entry.entry_id, dest)) - elif dest.action( + elif dest.hasAction( WMS_WebControl_pro_API_actionDescription.RollerShutterBlindDrive ): entities.append(WebControlProRollerShutter(config_entry.entry_id, dest)) diff --git a/homeassistant/components/wmspro/light.py b/homeassistant/components/wmspro/light.py index 52d092ed9f089c..2326734ceafbaa 100644 --- a/homeassistant/components/wmspro/light.py +++ b/homeassistant/components/wmspro/light.py @@ -33,9 +33,9 @@ async def async_setup_entry( entities: list[WebControlProGenericEntity] = [] for dest in hub.dests.values(): - if dest.action(WMS_WebControl_pro_API_actionDescription.LightDimming): + if dest.hasAction(WMS_WebControl_pro_API_actionDescription.LightDimming): entities.append(WebControlProDimmer(config_entry.entry_id, dest)) - elif dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch): + elif dest.hasAction(WMS_WebControl_pro_API_actionDescription.LightSwitch): entities.append(WebControlProLight(config_entry.entry_id, dest)) async_add_entities(entities) diff --git a/homeassistant/components/wmspro/manifest.json b/homeassistant/components/wmspro/manifest.json index 9185768165a8ae..9dbcf09a7d43bd 100644 --- a/homeassistant/components/wmspro/manifest.json +++ b/homeassistant/components/wmspro/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/wmspro", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["pywmspro==0.3.0"] + "requirements": ["pywmspro==0.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c9f708ad9abae9..56a1e7f8597bf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2596,7 +2596,7 @@ pywilight==0.0.74 pywizlight==0.6.3 # homeassistant.components.wmspro -pywmspro==0.3.0 +pywmspro==0.3.2 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e0ad947c38acc..bac418c6d59283 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2154,7 +2154,7 @@ pywilight==0.0.74 pywizlight==0.6.3 # homeassistant.components.wmspro -pywmspro==0.3.0 +pywmspro==0.3.2 # homeassistant.components.ws66i pyws66i==1.1 From 959c3a8a996ad485332752ecb5ccb4c90b8b2696 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 19 Jul 2025 10:49:14 -0700 Subject: [PATCH 77/96] Fix a bug in rainbird device migration that results in additional devices (#149078) --- homeassistant/components/rainbird/__init__.py | 3 + tests/components/rainbird/test_init.py | 72 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index f9cd751a81e0ef..e986cc302aee59 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -218,6 +218,9 @@ def _async_fix_device_id( for device_entry in device_entries: unique_id = str(next(iter(device_entry.identifiers))[1]) device_entry_map[unique_id] = device_entry + if unique_id.startswith(mac_address): + # Already in the correct format + continue if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id: migrations[unique_id] = f"{mac_address}{suffix}" diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index 01e0c4458e4f3c..520f8578c6ea47 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -449,3 +449,75 @@ async def test_fix_duplicate_device_ids( assert device_entry.identifiers == {(DOMAIN, MAC_ADDRESS_UNIQUE_ID)} assert device_entry.name_by_user == expected_device_name assert device_entry.disabled_by == expected_disabled_by + + +async def test_reload_migration_with_leading_zero_mac( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test migration and reload of a device with a mac address with a leading zero.""" + mac_address = "01:02:03:04:05:06" + mac_address_unique_id = dr.format_mac(mac_address) + serial_number = "0" + + # Setup the config entry to be in a pre-migrated state + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=serial_number, + data={ + "host": "127.0.0.1", + "password": "password", + CONF_MAC: mac_address, + "serial_number": serial_number, + }, + ) + config_entry.add_to_hass(hass) + + # Create a device and entity with the old unique id format + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, f"{serial_number}-1")}, + ) + entity_entry = entity_registry.async_get_or_create( + "switch", + DOMAIN, + f"{serial_number}-1-zone1", + suggested_object_id="zone1", + config_entry=config_entry, + device_id=device_entry.id, + ) + + # Setup the integration, which will migrate the unique ids + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify the device and entity were migrated to the new format + migrated_device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, f"{mac_address_unique_id}-1")} + ) + assert migrated_device_entry is not None + migrated_entity_entry = entity_registry.async_get(entity_entry.entity_id) + assert migrated_entity_entry is not None + assert migrated_entity_entry.unique_id == f"{mac_address_unique_id}-1-zone1" + + # Reload the integration + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify the device and entity still have the correct identifiers and were not duplicated + reloaded_device_entry = device_registry.async_get(migrated_device_entry.id) + assert reloaded_device_entry is not None + assert reloaded_device_entry.identifiers == {(DOMAIN, f"{mac_address_unique_id}-1")} + reloaded_entity_entry = entity_registry.async_get(entity_entry.entity_id) + assert reloaded_entity_entry is not None + assert reloaded_entity_entry.unique_id == f"{mac_address_unique_id}-1-zone1" + + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 1 + ) + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 1 + ) From fa207860a0582a54a44eb94d9d617402f4845257 Mon Sep 17 00:00:00 2001 From: hanwg Date: Tue, 22 Jul 2025 13:42:40 +0800 Subject: [PATCH 78/96] Fix multiple webhook secrets for Telegram bot (#149103) --- homeassistant/components/telegram_bot/webhooks.py | 14 ++++++++++---- tests/components/telegram_bot/test_telegram_bot.py | 12 ++++++------ tests/components/telegram_bot/test_webhooks.py | 5 +++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 0bfad34681a62b..29c3305858bb80 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -82,7 +82,7 @@ def __init__( self.base_url = config.data.get(CONF_URL) or get_url( hass, require_ssl=True, allow_internal=False ) - self.webhook_url = f"{self.base_url}{TELEGRAM_WEBHOOK_URL}" + self.webhook_url = self.base_url + _get_webhook_url(bot) async def shutdown(self) -> None: """Shutdown the app.""" @@ -98,9 +98,11 @@ async def _try_to_set_webhook(self) -> bool: api_kwargs={"secret_token": self.secret_token}, connect_timeout=5, ) - except TelegramError: + except TelegramError as err: retry_num += 1 - _LOGGER.warning("Error trying to set webhook (retry #%d)", retry_num) + _LOGGER.warning( + "Error trying to set webhook (retry #%d)", retry_num, exc_info=err + ) return False @@ -143,7 +145,6 @@ class PushBotView(HomeAssistantView): """View for handling webhook calls from Telegram.""" requires_auth = False - url = TELEGRAM_WEBHOOK_URL name = "telegram_webhooks" def __init__( @@ -160,6 +161,7 @@ def __init__( self.application = application self.trusted_networks = trusted_networks self.secret_token = secret_token + self.url = _get_webhook_url(bot) async def post(self, request: HomeAssistantRequest) -> Response | None: """Accept the POST from telegram.""" @@ -183,3 +185,7 @@ async def post(self, request: HomeAssistantRequest) -> Response | None: await self.application.process_update(update) return None + + +def _get_webhook_url(bot: Bot) -> str: + return f"{TELEGRAM_WEBHOOK_URL}_{bot.id}" diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index 73dd9e277634d0..80b9859ceabdd6 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -364,7 +364,7 @@ async def test_webhook_endpoint_generates_telegram_text_event( events = async_capture_events(hass, "telegram_text") response = await client.post( - TELEGRAM_WEBHOOK_URL, + f"{TELEGRAM_WEBHOOK_URL}_123456", json=update_message_text, headers={"X-Telegram-Bot-Api-Secret-Token": mock_generate_secret_token}, ) @@ -391,7 +391,7 @@ async def test_webhook_endpoint_generates_telegram_command_event( events = async_capture_events(hass, "telegram_command") response = await client.post( - TELEGRAM_WEBHOOK_URL, + f"{TELEGRAM_WEBHOOK_URL}_123456", json=update_message_command, headers={"X-Telegram-Bot-Api-Secret-Token": mock_generate_secret_token}, ) @@ -418,7 +418,7 @@ async def test_webhook_endpoint_generates_telegram_callback_event( events = async_capture_events(hass, "telegram_callback") response = await client.post( - TELEGRAM_WEBHOOK_URL, + f"{TELEGRAM_WEBHOOK_URL}_123456", json=update_callback_query, headers={"X-Telegram-Bot-Api-Secret-Token": mock_generate_secret_token}, ) @@ -594,7 +594,7 @@ async def test_webhook_endpoint_unauthorized_update_doesnt_generate_telegram_tex events = async_capture_events(hass, "telegram_text") response = await client.post( - TELEGRAM_WEBHOOK_URL, + f"{TELEGRAM_WEBHOOK_URL}_123456", json=unauthorized_update_message_text, headers={"X-Telegram-Bot-Api-Secret-Token": mock_generate_secret_token}, ) @@ -618,7 +618,7 @@ async def test_webhook_endpoint_without_secret_token_is_denied( async_capture_events(hass, "telegram_text") response = await client.post( - TELEGRAM_WEBHOOK_URL, + f"{TELEGRAM_WEBHOOK_URL}_123456", json=update_message_text, ) assert response.status == 401 @@ -636,7 +636,7 @@ async def test_webhook_endpoint_invalid_secret_token_is_denied( async_capture_events(hass, "telegram_text") response = await client.post( - TELEGRAM_WEBHOOK_URL, + f"{TELEGRAM_WEBHOOK_URL}_123456", json=update_message_text, headers={"X-Telegram-Bot-Api-Secret-Token": incorrect_secret_token}, ) diff --git a/tests/components/telegram_bot/test_webhooks.py b/tests/components/telegram_bot/test_webhooks.py index 3419d33074deaf..a02bb3e3358606 100644 --- a/tests/components/telegram_bot/test_webhooks.py +++ b/tests/components/telegram_bot/test_webhooks.py @@ -7,6 +7,7 @@ from telegram import WebhookInfo from telegram.error import TimedOut +from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -115,7 +116,7 @@ async def test_webhooks_update_invalid_json( client = await hass_client() response = await client.post( - "/api/telegram_webhooks", + f"{TELEGRAM_WEBHOOK_URL}_123456", headers={"X-Telegram-Bot-Api-Secret-Token": mock_generate_secret_token}, ) assert response.status == 400 @@ -139,7 +140,7 @@ async def test_webhooks_unauthorized_network( return_value=IPv4Network("1.2.3.4"), ) as mock_remote: response = await client.post( - "/api/telegram_webhooks", + f"{TELEGRAM_WEBHOOK_URL}_123456", json="mock json", headers={"X-Telegram-Bot-Api-Secret-Token": mock_generate_secret_token}, ) From f428ffde872e4fb078479579a985c17520139da7 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 21 Jul 2025 01:25:45 -0400 Subject: [PATCH 79/96] Bump pyschlage to 2025.7.2 (#149148) --- 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 893c30dfd419d8..c5b91cefd2e7ee 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.4.0"] + "requirements": ["pyschlage==2025.7.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 56a1e7f8597bf2..0b6fa6d9685f2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2309,7 +2309,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2025.4.0 +pyschlage==2025.7.2 # homeassistant.components.sensibo pysensibo==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bac418c6d59283..0917f77dc27012 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1921,7 +1921,7 @@ pyrympro==0.0.9 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2025.4.0 +pyschlage==2025.7.2 # homeassistant.components.sensibo pysensibo==1.2.1 From c3eb6dea1132f9957a454b8291863cbe50ee8142 Mon Sep 17 00:00:00 2001 From: jvmahon Date: Fri, 25 Jul 2025 13:09:58 -0400 Subject: [PATCH 80/96] Fix Matter light get brightness (#149186) --- homeassistant/components/matter/light.py | 7 ++++++- tests/components/matter/test_light.py | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index c61fd0879fa2ee..a86938730c98c5 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -5,6 +5,7 @@ from typing import Any from chip.clusters import Objects as clusters +from chip.clusters.Objects import NullValue from matter_server.client.models import device_types from homeassistant.components.light import ( @@ -241,7 +242,7 @@ def _get_color_temperature(self) -> int: return int(color_temp) - def _get_brightness(self) -> int: + def _get_brightness(self) -> int | None: """Get brightness from matter.""" level_control = self._endpoint.get_cluster(clusters.LevelControl) @@ -255,6 +256,10 @@ def _get_brightness(self) -> int: self.entity_id, ) + if level_control.currentLevel is NullValue: + # currentLevel is a nullable value. + return None + return round( renormalize( level_control.currentLevel, diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index b600ededa6e545..f9abf986170d48 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -131,6 +131,15 @@ async def test_dimmable_light( ) -> None: """Test a dimmable light.""" + # Test for currentLevel is None + set_node_attribute(matter_node, 1, 8, 0, None) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "on" + assert state.attributes["brightness"] is None + # Test that the light brightness is 50 (out of 254) set_node_attribute(matter_node, 1, 8, 0, 50) await trigger_subscription_callback(hass, matter_client) From 68b7d094760520819033cc982ba17d4f89232e81 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 24 Jul 2025 00:55:44 +1000 Subject: [PATCH 81/96] Fix brightness_step and brightness_step_pct via lifx.set_state (#149217) Signed-off-by: Avi Miller --- homeassistant/components/lifx/light.py | 17 ++++++++++ tests/components/lifx/test_light.py | 44 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 3d30fcd369e4f2..7a1b51ac8aeb77 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -10,6 +10,9 @@ import voluptuous as vol from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, ATTR_EFFECT, ATTR_TRANSITION, LIGHT_TURN_ON_SCHEMA, @@ -234,6 +237,20 @@ async def set_state(self, **kwargs: Any) -> None: else: fade = 0 + if ATTR_BRIGHTNESS_STEP in kwargs or ATTR_BRIGHTNESS_STEP_PCT in kwargs: + brightness = self.brightness if self.is_on and self.brightness else 0 + + if ATTR_BRIGHTNESS_STEP in kwargs: + brightness += kwargs.pop(ATTR_BRIGHTNESS_STEP) + + else: + brightness_pct = round(brightness / 255 * 100) + brightness = round( + (brightness_pct + kwargs.pop(ATTR_BRIGHTNESS_STEP_PCT)) / 100 * 255 + ) + + kwargs[ATTR_BRIGHTNESS] = max(0, min(255, brightness)) + # These are both False if ATTR_POWER is not set power_on = kwargs.get(ATTR_POWER, False) power_off = not kwargs.get(ATTR_POWER, True) diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index d66908c1b1aeb0..edb13c259e8225 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -30,6 +30,8 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_MODE, ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, @@ -1735,6 +1737,48 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None: bulb.set_color.reset_mock() +async def test_lifx_set_state_brightness(hass: HomeAssistant) -> None: + """Test lifx.set_state works with brightness, brightness_pct and brightness_step.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [0, 0, 32768, 3500] + with ( + _patch_discovery(device=bulb), + _patch_config_flow_try_connect(device=bulb), + _patch_device(device=bulb), + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + # brightness_step should convert from 8 bit to 16 bit + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_STEP: 128}, + blocking=True, + ) + + assert bulb.set_color.calls[0][0][0] == [0, 0, 65535, 3500] + bulb.set_color.reset_mock() + + # brightness_step_pct should convert from percentage to 16 bit + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_STEP_PCT: 50}, + blocking=True, + ) + + assert bulb.set_color.calls[0][0][0] == [0, 0, 65535, 3500] + bulb.set_color.reset_mock() + + async def test_lifx_set_state_color(hass: HomeAssistant) -> None: """Test lifx.set_state works with color names and RGB.""" config_entry = MockConfigEntry( From 60f4d29d608d4f83cdaa9d202a1e1439841267bb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 22 Jul 2025 16:09:11 +0200 Subject: [PATCH 82/96] Add Z-Wave USB migration confirm step (#149243) --- homeassistant/components/zwave_js/config_flow.py | 15 ++++++++++++++- homeassistant/components/zwave_js/strings.json | 4 ++++ tests/components/zwave_js/test_config_flow.py | 10 ++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 7e95e274713793..925946ed7f1b3e 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -495,10 +495,23 @@ async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResu self._usb_discovery = True if current_config_entries: - return await self.async_step_intent_migrate() + return await self.async_step_confirm_usb_migration() return await self.async_step_installation_type() + async def async_step_confirm_usb_migration( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm USB migration.""" + if user_input is not None: + return await self.async_step_intent_migrate() + return self.async_show_form( + step_id="confirm_usb_migration", + description_placeholders={ + "usb_title": self.context["title_placeholders"][CONF_NAME], + }, + ) + async def async_step_manual( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 7445182e5f6bec..835dfbf74716f4 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -108,6 +108,10 @@ "start_addon": { "title": "Configuring add-on" }, + "confirm_usb_migration": { + "description": "You are about to migrate your Z-Wave network from the old adapter to the new adapter {usb_title}. This will take a backup of the network from the old adapter and restore the network to the new adapter.\n\nPress Submit to continue with the migration.", + "title": "Migrate to a new adapter" + }, "zeroconf_confirm": { "description": "Do you want to add the Z-Wave Server with home ID {home_id} found at {url} to Home Assistant?", "title": "Discovered Z-Wave Server" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index a1642746d0335f..c708b1c9d66cfa 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -932,6 +932,11 @@ async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): assert mock_usb_serial_by_id.call_count == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_usb_migration" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" @@ -1049,6 +1054,11 @@ async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): assert mock_usb_serial_by_id.call_count == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_usb_migration" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" From 4c8ab8eb6402bcfe896aa7ca2c84e7c6aec0aeb4 Mon Sep 17 00:00:00 2001 From: Antoine Reversat Date: Wed, 23 Jul 2025 08:52:14 -0400 Subject: [PATCH 83/96] Add fan off mode to the supported fan modes to fujitsu_fglair (#149277) --- homeassistant/components/fujitsu_fglair/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fujitsu_fglair/climate.py b/homeassistant/components/fujitsu_fglair/climate.py index bf1df07823c0b6..85ef119a5831ef 100644 --- a/homeassistant/components/fujitsu_fglair/climate.py +++ b/homeassistant/components/fujitsu_fglair/climate.py @@ -15,6 +15,7 @@ FAN_HIGH, FAN_LOW, FAN_MEDIUM, + FAN_OFF, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, @@ -31,6 +32,7 @@ from .entity import FGLairEntity HA_TO_FUJI_FAN = { + FAN_OFF: FanSpeed.QUIET, FAN_LOW: FanSpeed.LOW, FAN_MEDIUM: FanSpeed.MEDIUM, FAN_HIGH: FanSpeed.HIGH, From cd800da357db61799ec66120e5805d1e7d6f1b51 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 23 Jul 2025 22:45:50 +1000 Subject: [PATCH 84/96] Update Tesla OAuth Server in Tesla Fleet (#149280) --- homeassistant/components/tesla_fleet/const.py | 5 ++--- tests/components/tesla_fleet/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tesla_fleet/const.py b/homeassistant/components/tesla_fleet/const.py index d73234b1fdd4cd..761bbebf7a8316 100644 --- a/homeassistant/components/tesla_fleet/const.py +++ b/homeassistant/components/tesla_fleet/const.py @@ -14,9 +14,8 @@ LOGGER = logging.getLogger(__package__) -CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d" -AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize" -TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token" +AUTHORIZE_URL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/authorize" +TOKEN_URL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token" SCOPES = [ Scope.OPENID, diff --git a/tests/components/tesla_fleet/__init__.py b/tests/components/tesla_fleet/__init__.py index c51cd83ee66c05..a43ec14fc51f23 100644 --- a/tests/components/tesla_fleet/__init__.py +++ b/tests/components/tesla_fleet/__init__.py @@ -8,7 +8,7 @@ ClientCredential, async_import_client_credential, ) -from homeassistant.components.tesla_fleet.const import CLIENT_ID, DOMAIN +from homeassistant.components.tesla_fleet.const import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -28,7 +28,7 @@ async def setup_platform( await async_import_client_credential( hass, DOMAIN, - ClientCredential(CLIENT_ID, "", "Home Assistant"), + ClientCredential("CLIENT_ID", "CLIENT_SECRET", "Home Assistant"), DOMAIN, ) From 4a7d06a68a44546d3277e207158cac5c0bad4bc4 Mon Sep 17 00:00:00 2001 From: Alex Hermann Date: Sun, 27 Jul 2025 19:18:00 +0200 Subject: [PATCH 85/96] Update slixmpp to 1.10.0 (#149374) --- homeassistant/components/xmpp/manifest.json | 2 +- homeassistant/components/xmpp/notify.py | 5 +++-- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index d77d70ff86c036..d128e3e511127f 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -6,5 +6,5 @@ "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"], "quality_scale": "legacy", - "requirements": ["slixmpp==1.8.5", "emoji==2.8.0"] + "requirements": ["slixmpp==1.10.0", "emoji==2.8.0"] } diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 968f925d1e8809..c9829746d59a07 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -144,7 +144,8 @@ def __init__(self): self.loop = hass.loop - self.force_starttls = use_tls + self.enable_starttls = use_tls + self.enable_direct_tls = 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) @@ -163,7 +164,7 @@ def __init__(self): self.register_plugin("xep_0128") # Service Discovery self.register_plugin("xep_0363") # HTTP upload - self.connect(force_starttls=self.force_starttls, use_ssl=False) + self.connect() async def start(self, event): """Start the communication and sends the message.""" diff --git a/requirements_all.txt b/requirements_all.txt index 0b6fa6d9685f2e..dbc6d309c15045 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2786,7 +2786,7 @@ skyboxremote==0.0.6 slack_sdk==3.33.4 # homeassistant.components.xmpp -slixmpp==1.8.5 +slixmpp==1.10.0 # homeassistant.components.smart_meter_texas smart-meter-texas==0.5.5 From dc6d2e3e8462f838435fc7ce3c83b78099e7fa23 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 24 Jul 2025 18:54:00 +0300 Subject: [PATCH 86/96] Bump aioamazondevices to 3.5.1 (#149385) --- homeassistant/components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 6904f8000ce88c..c1eb3369c117ff 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.5.0"] + "requirements": ["aioamazondevices==3.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index dbc6d309c15045..78aa0cc4f6c838 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.5.0 +aioamazondevices==3.5.1 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0917f77dc27012..f4c0f06175f19a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.5.0 +aioamazondevices==3.5.1 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 725799c73ef46a58ef0217b788d3a29ede0a10a6 Mon Sep 17 00:00:00 2001 From: jb101010-2 <168106462+jb101010-2@users.noreply.github.com> Date: Sat, 26 Jul 2025 12:38:38 +0200 Subject: [PATCH 87/96] Bump pysuezV2 to 2.0.7 (#149436) --- homeassistant/components/suez_water/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/suez_water/conftest.py | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 9149f216563dc1..5c23240ce9196f 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["pysuez", "regex"], "quality_scale": "bronze", - "requirements": ["pysuezV2==2.0.5"] + "requirements": ["pysuezV2==2.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 78aa0cc4f6c838..be200069c5091b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2384,7 +2384,7 @@ pysqueezebox==0.12.1 pystiebeleltron==0.1.0 # homeassistant.components.suez_water -pysuezV2==2.0.5 +pysuezV2==2.0.7 # homeassistant.components.switchbee pyswitchbee==1.8.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4c0f06175f19a..e978a39f58b51c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1987,7 +1987,7 @@ pysqueezebox==0.12.1 pystiebeleltron==0.1.0 # homeassistant.components.suez_water -pysuezV2==2.0.5 +pysuezV2==2.0.7 # homeassistant.components.switchbee pyswitchbee==1.8.3 diff --git a/tests/components/suez_water/conftest.py b/tests/components/suez_water/conftest.py index 9d29191289e068..005c14b7458456 100644 --- a/tests/components/suez_water/conftest.py +++ b/tests/components/suez_water/conftest.py @@ -87,5 +87,7 @@ def mock_suez_client(recorder_mock: Recorder) -> Generator[AsyncMock]: ) suez_client.fetch_aggregated_data.return_value = result - suez_client.get_price.return_value = PriceResult("4.74") + suez_client.get_price.return_value = PriceResult( + "OK", {"price": 4.74}, "Price is 4.74" + ) yield suez_client From f0cb5d54808e950f8982efb5f0d79b6e9e9a0a7d Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sun, 27 Jul 2025 19:09:13 +0200 Subject: [PATCH 88/96] Bump habiticalib to v0.4.1 (#149523) --- 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 8b03e5efe014b0..d890ed23676746 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.0"] + "requirements": ["habiticalib==0.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index be200069c5091b..6eb24ec184bdff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1121,7 +1121,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.0 +habiticalib==0.4.1 # homeassistant.components.bluetooth habluetooth==3.49.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e978a39f58b51c..ebd3f9e863b110 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -982,7 +982,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.0 +habiticalib==0.4.1 # homeassistant.components.bluetooth habluetooth==3.49.0 From d384bee5767f248f383a2fa0f1bb1681ff32a2c2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Jul 2025 17:35:19 +0000 Subject: [PATCH 89/96] Bump version to 2025.7.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 62e6d49befd225..6db2d238a98b00 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 7 -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 c010ecb725488e..03ad9af59a192b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.7.3" +version = "2025.7.4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 09e7d8d1a5525cdb4ab51bc58c496d8bc32a6246 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 29 Jul 2025 17:42:26 +0200 Subject: [PATCH 90/96] Increase open file descriptor limit on startup (#148940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Čermák Co-authored-by: Martin Hjelmare --- homeassistant/runner.py | 2 + homeassistant/util/resource.py | 65 ++++++++++++++ tests/util/test_resource.py | 153 +++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 homeassistant/util/resource.py create mode 100644 tests/util/test_resource.py diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 59775655854504..abcf32f2659cd0 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -17,6 +17,7 @@ from .core import callback from .helpers.frame import warn_use from .util.executor import InterruptibleThreadPoolExecutor +from .util.resource import set_open_file_descriptor_limit from .util.thread import deadlock_safe_shutdown # @@ -146,6 +147,7 @@ def _enable_posix_spawn() -> None: def run(runtime_config: RuntimeConfig) -> int: """Run Home Assistant.""" _enable_posix_spawn() + set_open_file_descriptor_limit() asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) # Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out loop = asyncio.new_event_loop() diff --git a/homeassistant/util/resource.py b/homeassistant/util/resource.py new file mode 100644 index 00000000000000..41982df9e506e1 --- /dev/null +++ b/homeassistant/util/resource.py @@ -0,0 +1,65 @@ +"""Resource management utilities for Home Assistant.""" + +from __future__ import annotations + +import logging +import os +import resource +from typing import Final + +_LOGGER = logging.getLogger(__name__) + +# Default soft file descriptor limit to set +DEFAULT_SOFT_FILE_LIMIT: Final = 2048 + + +def set_open_file_descriptor_limit() -> None: + """Set the maximum open file descriptor soft limit.""" + try: + # Check environment variable first, then use default + soft_limit = int(os.environ.get("SOFT_FILE_LIMIT", DEFAULT_SOFT_FILE_LIMIT)) + + # Get current limits + current_soft, current_hard = resource.getrlimit(resource.RLIMIT_NOFILE) + + _LOGGER.debug( + "Current file descriptor limits: soft=%d, hard=%d", + current_soft, + current_hard, + ) + + # Don't increase if already at or above the desired limit + if current_soft >= soft_limit: + _LOGGER.debug( + "Current soft limit (%d) is already >= desired limit (%d), skipping", + current_soft, + soft_limit, + ) + return + + # Don't set soft limit higher than hard limit + if soft_limit > current_hard: + _LOGGER.warning( + "Requested soft limit (%d) exceeds hard limit (%d), " + "setting to hard limit", + soft_limit, + current_hard, + ) + soft_limit = current_hard + + # Set the new soft limit + resource.setrlimit(resource.RLIMIT_NOFILE, (soft_limit, current_hard)) + + # Verify the change + new_soft, new_hard = resource.getrlimit(resource.RLIMIT_NOFILE) + _LOGGER.info( + "File descriptor limits updated: soft=%d->%d, hard=%d", + current_soft, + new_soft, + new_hard, + ) + + except OSError as err: + _LOGGER.error("Failed to set file descriptor limit: %s", err) + except ValueError as err: + _LOGGER.error("Invalid file descriptor limit value: %s", err) diff --git a/tests/util/test_resource.py b/tests/util/test_resource.py new file mode 100644 index 00000000000000..a32ceb1062ce48 --- /dev/null +++ b/tests/util/test_resource.py @@ -0,0 +1,153 @@ +"""Test the resource utility module.""" + +import os +import resource +from unittest.mock import call, patch + +import pytest + +from homeassistant.util.resource import ( + DEFAULT_SOFT_FILE_LIMIT, + set_open_file_descriptor_limit, +) + + +@pytest.mark.parametrize( + ("original_soft", "expected_calls", "should_log_already_sufficient"), + [ + ( + 1024, + [call(resource.RLIMIT_NOFILE, (DEFAULT_SOFT_FILE_LIMIT, 524288))], + False, + ), + ( + DEFAULT_SOFT_FILE_LIMIT - 1, + [call(resource.RLIMIT_NOFILE, (DEFAULT_SOFT_FILE_LIMIT, 524288))], + False, + ), + (DEFAULT_SOFT_FILE_LIMIT, [], True), + (DEFAULT_SOFT_FILE_LIMIT + 1, [], True), + ], +) +def test_set_open_file_descriptor_limit_default( + caplog: pytest.LogCaptureFixture, + original_soft: int, + expected_calls: list, + should_log_already_sufficient: bool, +) -> None: + """Test setting file limit with default value.""" + original_hard = 524288 + with ( + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(original_soft, original_hard), + ), + patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit, + ): + set_open_file_descriptor_limit() + + assert mock_setrlimit.call_args_list == expected_calls + assert ( + f"Current soft limit ({original_soft}) is already" in caplog.text + ) is should_log_already_sufficient + + +@pytest.mark.parametrize( + ( + "original_soft", + "custom_limit", + "expected_calls", + "should_log_already_sufficient", + ), + [ + (1499, 1500, [call(resource.RLIMIT_NOFILE, (1500, 524288))], False), + (1500, 1500, [], True), + (1501, 1500, [], True), + ], +) +def test_set_open_file_descriptor_limit_environment_variable( + caplog: pytest.LogCaptureFixture, + original_soft: int, + custom_limit: int, + expected_calls: list, + should_log_already_sufficient: bool, +) -> None: + """Test setting file limit from environment variable.""" + original_hard = 524288 + with ( + patch.dict(os.environ, {"SOFT_FILE_LIMIT": str(custom_limit)}), + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(original_soft, original_hard), + ), + patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit, + ): + set_open_file_descriptor_limit() + + assert mock_setrlimit.call_args_list == expected_calls + assert ( + f"Current soft limit ({original_soft}) is already" in caplog.text + ) is should_log_already_sufficient + + +def test_set_open_file_descriptor_limit_exceeds_hard_limit( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setting file limit that exceeds hard limit.""" + original_soft, original_hard = (1024, 524288) + excessive_limit = original_hard + 1 + + with ( + patch.dict(os.environ, {"SOFT_FILE_LIMIT": str(excessive_limit)}), + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(original_soft, original_hard), + ), + patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit, + ): + set_open_file_descriptor_limit() + + mock_setrlimit.assert_called_once_with( + resource.RLIMIT_NOFILE, (original_hard, original_hard) + ) + assert ( + f"Requested soft limit ({excessive_limit}) exceeds hard limit ({original_hard})" + in caplog.text + ) + + +def test_set_open_file_descriptor_limit_os_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling OSError when setting file limit.""" + with ( + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(1024, 524288), + ), + patch( + "homeassistant.util.resource.resource.setrlimit", + side_effect=OSError("Permission denied"), + ), + ): + set_open_file_descriptor_limit() + + assert "Failed to set file descriptor limit" in caplog.text + assert "Permission denied" in caplog.text + + +def test_set_open_file_descriptor_limit_value_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling ValueError when setting file limit.""" + with ( + patch.dict(os.environ, {"SOFT_FILE_LIMIT": "invalid_value"}), + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(1024, 524288), + ), + ): + set_open_file_descriptor_limit() + + assert "Invalid file descriptor limit value" in caplog.text + assert "'invalid_value'" in caplog.text From 25407c0f4b5d3ef0e3e9faaf1c74cc6b466cbd5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Jul 2025 07:21:31 -1000 Subject: [PATCH 91/96] Bump aiohttp to 3.12.15 (#149609) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a43eadce0dea37..24c107e56114c9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.5.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.12.14 +aiohttp==3.12.15 aiohttp_cors==0.8.1 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index d15a93fd8bde79..35a2bf2c7fb096 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.14", + "aiohttp==3.12.15", "aiohttp_cors==0.8.1", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 6110854f5f6d3a..a332eb930c2115 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.5.0 aiohasupervisor==0.3.1 -aiohttp==3.12.14 +aiohttp==3.12.15 aiohttp_cors==0.8.1 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 From b67e85e8dae75e52e5b56e0ca616364816277432 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 29 Jul 2025 19:41:13 +0200 Subject: [PATCH 92/96] Introduce Ubiquiti UISP airOS (#148989) Co-authored-by: Norbert Rittel Co-authored-by: Joost Lekkerkerker --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/brands/ubiquiti.json | 2 +- homeassistant/components/airos/__init__.py | 42 ++ homeassistant/components/airos/config_flow.py | 82 +++ homeassistant/components/airos/const.py | 9 + homeassistant/components/airos/coordinator.py | 66 +++ homeassistant/components/airos/entity.py | 36 ++ homeassistant/components/airos/manifest.json | 10 + .../components/airos/quality_scale.yaml | 72 +++ homeassistant/components/airos/sensor.py | 152 +++++ homeassistant/components/airos/strings.json | 87 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/airos/__init__.py | 13 + tests/components/airos/conftest.py | 61 ++ .../airos/fixtures/airos_ap-ptp.json | 300 ++++++++++ .../airos/snapshots/test_sensor.ambr | 547 ++++++++++++++++++ tests/components/airos/test_config_flow.py | 119 ++++ tests/components/airos/test_sensor.py | 85 +++ 23 files changed, 1708 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airos/__init__.py create mode 100644 homeassistant/components/airos/config_flow.py create mode 100644 homeassistant/components/airos/const.py create mode 100644 homeassistant/components/airos/coordinator.py create mode 100644 homeassistant/components/airos/entity.py create mode 100644 homeassistant/components/airos/manifest.json create mode 100644 homeassistant/components/airos/quality_scale.yaml create mode 100644 homeassistant/components/airos/sensor.py create mode 100644 homeassistant/components/airos/strings.json create mode 100644 tests/components/airos/__init__.py create mode 100644 tests/components/airos/conftest.py create mode 100644 tests/components/airos/fixtures/airos_ap-ptp.json create mode 100644 tests/components/airos/snapshots/test_sensor.ambr create mode 100644 tests/components/airos/test_config_flow.py create mode 100644 tests/components/airos/test_sensor.py diff --git a/.strict-typing b/.strict-typing index c6e27a011f1b07..c125e85bbfc4ce 100644 --- a/.strict-typing +++ b/.strict-typing @@ -53,6 +53,7 @@ homeassistant.components.air_quality.* homeassistant.components.airgradient.* homeassistant.components.airly.* homeassistant.components.airnow.* +homeassistant.components.airos.* homeassistant.components.airq.* homeassistant.components.airthings.* homeassistant.components.airthings_ble.* diff --git a/CODEOWNERS b/CODEOWNERS index 4e7c1b9175a734..5ef8479d4d388f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -67,6 +67,8 @@ build.json @home-assistant/supervisor /tests/components/airly/ @bieniu /homeassistant/components/airnow/ @asymworks /tests/components/airnow/ @asymworks +/homeassistant/components/airos/ @CoMPaTech +/tests/components/airos/ @CoMPaTech /homeassistant/components/airq/ @Sibgatulin @dl2080 /tests/components/airq/ @Sibgatulin @dl2080 /homeassistant/components/airthings/ @danielhiversen @LaStrada diff --git a/homeassistant/brands/ubiquiti.json b/homeassistant/brands/ubiquiti.json index 8b64cffaa7e59e..bb345775a60491 100644 --- a/homeassistant/brands/ubiquiti.json +++ b/homeassistant/brands/ubiquiti.json @@ -1,5 +1,5 @@ { "domain": "ubiquiti", "name": "Ubiquiti", - "integrations": ["unifi", "unifi_direct", "unifiled", "unifiprotect"] + "integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"] } diff --git a/homeassistant/components/airos/__init__.py b/homeassistant/components/airos/__init__.py new file mode 100644 index 00000000000000..54f0db205a9c35 --- /dev/null +++ b/homeassistant/components/airos/__init__.py @@ -0,0 +1,42 @@ +"""The Ubiquiti airOS integration.""" + +from __future__ import annotations + +from airos.airos8 import AirOS + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator + +_PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: + """Set up Ubiquiti airOS from a config entry.""" + + # By default airOS 8 comes with self-signed SSL certificates, + # with no option in the web UI to change or upload a custom certificate. + session = async_get_clientsession(hass, verify_ssl=False) + + airos_device = AirOS( + host=entry.data[CONF_HOST], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + session=session, + ) + + coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device) + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS) diff --git a/homeassistant/components/airos/config_flow.py b/homeassistant/components/airos/config_flow.py new file mode 100644 index 00000000000000..287f54101c8093 --- /dev/null +++ b/homeassistant/components/airos/config_flow.py @@ -0,0 +1,82 @@ +"""Config flow for the Ubiquiti airOS integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from airos.exceptions import ( + ConnectionAuthenticationError, + ConnectionSetupError, + DataMissingError, + DeviceConnectionError, + KeyDataMissingError, +) +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN +from .coordinator import AirOS + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME, default="ubnt"): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class AirOSConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ubiquiti airOS.""" + + VERSION = 1 + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + # By default airOS 8 comes with self-signed SSL certificates, + # with no option in the web UI to change or upload a custom certificate. + session = async_get_clientsession(self.hass, verify_ssl=False) + + airos_device = AirOS( + host=user_input[CONF_HOST], + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + session=session, + ) + try: + await airos_device.login() + airos_data = await airos_device.status() + + except ( + ConnectionSetupError, + DeviceConnectionError, + ): + errors["base"] = "cannot_connect" + except (ConnectionAuthenticationError, DataMissingError): + errors["base"] = "invalid_auth" + except KeyDataMissingError: + errors["base"] = "key_data_missing" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(airos_data.derived.mac) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=airos_data.host.hostname, data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/airos/const.py b/homeassistant/components/airos/const.py new file mode 100644 index 00000000000000..f4be2594613c6e --- /dev/null +++ b/homeassistant/components/airos/const.py @@ -0,0 +1,9 @@ +"""Constants for the Ubiquiti airOS integration.""" + +from datetime import timedelta + +DOMAIN = "airos" + +SCAN_INTERVAL = timedelta(minutes=1) + +MANUFACTURER = "Ubiquiti" diff --git a/homeassistant/components/airos/coordinator.py b/homeassistant/components/airos/coordinator.py new file mode 100644 index 00000000000000..3f0f1a1238034c --- /dev/null +++ b/homeassistant/components/airos/coordinator.py @@ -0,0 +1,66 @@ +"""DataUpdateCoordinator for AirOS.""" + +from __future__ import annotations + +import logging + +from airos.airos8 import AirOS, AirOSData +from airos.exceptions import ( + ConnectionAuthenticationError, + ConnectionSetupError, + DataMissingError, + DeviceConnectionError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, SCAN_INTERVAL + +_LOGGER = logging.getLogger(__name__) + +type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator] + + +class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]): + """Class to manage fetching AirOS data from single endpoint.""" + + config_entry: AirOSConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS + ) -> None: + """Initialize the coordinator.""" + self.airos_device = airos_device + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> AirOSData: + """Fetch data from AirOS.""" + try: + await self.airos_device.login() + return await self.airos_device.status() + except (ConnectionAuthenticationError,) as err: + _LOGGER.exception("Error authenticating with airOS device") + raise ConfigEntryError( + translation_domain=DOMAIN, translation_key="invalid_auth" + ) from err + except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err: + _LOGGER.error("Error connecting to airOS device: %s", err) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from err + except (DataMissingError,) as err: + _LOGGER.error("Expected data not returned by airOS device: %s", err) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="error_data_missing", + ) from err diff --git a/homeassistant/components/airos/entity.py b/homeassistant/components/airos/entity.py new file mode 100644 index 00000000000000..e54962110fc155 --- /dev/null +++ b/homeassistant/components/airos/entity.py @@ -0,0 +1,36 @@ +"""Generic AirOS Entity Class.""" + +from __future__ import annotations + +from homeassistant.const import CONF_HOST +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import AirOSDataUpdateCoordinator + + +class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]): + """Represent a AirOS Entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: AirOSDataUpdateCoordinator) -> None: + """Initialise the gateway.""" + super().__init__(coordinator) + + airos_data = self.coordinator.data + + configuration_url: str | None = ( + f"https://{coordinator.config_entry.data[CONF_HOST]}" + ) + + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)}, + configuration_url=configuration_url, + identifiers={(DOMAIN, str(airos_data.host.device_id))}, + manufacturer=MANUFACTURER, + model=airos_data.host.devmodel, + name=airos_data.host.hostname, + sw_version=airos_data.host.fwversion, + ) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json new file mode 100644 index 00000000000000..cb6119a6fa99cc --- /dev/null +++ b/homeassistant/components/airos/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "airos", + "name": "Ubiquiti airOS", + "codeowners": ["@CoMPaTech"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airos", + "iot_class": "local_polling", + "quality_scale": "bronze", + "requirements": ["airos==0.2.1"] +} diff --git a/homeassistant/components/airos/quality_scale.yaml b/homeassistant/components/airos/quality_scale.yaml new file mode 100644 index 00000000000000..a0bacd5ebba307 --- /dev/null +++ b/homeassistant/components/airos/quality_scale.yaml @@ -0,0 +1,72 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: airOS does not have actions + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: airOS does not have actions + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: local_polling without events + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: airOS does not have actions + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: todo + integration-owner: done + log-when-unavailable: todo + parallel-updates: todo + reauthentication-flow: todo + test-coverage: done + + # Gold + devices: done + diagnostics: todo + discovery-update-info: todo + discovery: todo + docs-data-update: done + docs-examples: todo + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: todo + docs-troubleshooting: done + docs-use-cases: todo + dynamic-devices: todo + entity-category: done + entity-device-class: done + entity-disabled-by-default: + status: todo + comment: prepared binary_sensors will provide this + entity-translations: done + exception-translations: done + icon-translations: + status: exempt + comment: no (custom) icons used or envisioned + reconfiguration-flow: todo + repair-issues: todo + stale-devices: todo + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: done diff --git a/homeassistant/components/airos/sensor.py b/homeassistant/components/airos/sensor.py new file mode 100644 index 00000000000000..690bf21fc8ea2d --- /dev/null +++ b/homeassistant/components/airos/sensor.py @@ -0,0 +1,152 @@ +"""AirOS Sensor component for Home Assistant.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from airos.data import NetRole, WirelessMode + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS, + UnitOfDataRate, + UnitOfFrequency, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator +from .entity import AirOSEntity + +_LOGGER = logging.getLogger(__name__) + +WIRELESS_MODE_OPTIONS = [mode.value.replace("-", "_").lower() for mode in WirelessMode] +NETROLE_OPTIONS = [mode.value for mode in NetRole] + + +@dataclass(frozen=True, kw_only=True) +class AirOSSensorEntityDescription(SensorEntityDescription): + """Describe an AirOS sensor.""" + + value_fn: Callable[[AirOSData], StateType] + + +SENSORS: tuple[AirOSSensorEntityDescription, ...] = ( + AirOSSensorEntityDescription( + key="host_cpuload", + translation_key="host_cpuload", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.host.cpuload, + entity_registry_enabled_default=False, + ), + AirOSSensorEntityDescription( + key="host_netrole", + translation_key="host_netrole", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: data.host.netrole.value, + options=NETROLE_OPTIONS, + ), + AirOSSensorEntityDescription( + key="wireless_frequency", + translation_key="wireless_frequency", + native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.frequency, + ), + AirOSSensorEntityDescription( + key="wireless_essid", + translation_key="wireless_essid", + value_fn=lambda data: data.wireless.essid, + ), + AirOSSensorEntityDescription( + key="wireless_mode", + translation_key="wireless_mode", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(), + options=WIRELESS_MODE_OPTIONS, + ), + AirOSSensorEntityDescription( + key="wireless_antenna_gain", + translation_key="wireless_antenna_gain", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.antenna_gain, + ), + AirOSSensorEntityDescription( + key="wireless_throughput_tx", + translation_key="wireless_throughput_tx", + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.throughput.tx, + ), + AirOSSensorEntityDescription( + key="wireless_throughput_rx", + translation_key="wireless_throughput_rx", + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.throughput.rx, + ), + AirOSSensorEntityDescription( + key="wireless_polling_dl_capacity", + translation_key="wireless_polling_dl_capacity", + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.polling.dl_capacity, + ), + AirOSSensorEntityDescription( + key="wireless_polling_ul_capacity", + translation_key="wireless_polling_ul_capacity", + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.polling.ul_capacity, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AirOSConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the AirOS sensors from a config entry.""" + coordinator = config_entry.runtime_data + + async_add_entities(AirOSSensor(coordinator, description) for description in SENSORS) + + +class AirOSSensor(AirOSEntity, SensorEntity): + """Representation of a Sensor.""" + + entity_description: AirOSSensorEntityDescription + + def __init__( + self, + coordinator: AirOSDataUpdateCoordinator, + description: AirOSSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}" + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/airos/strings.json b/homeassistant/components/airos/strings.json new file mode 100644 index 00000000000000..6823ba8520b0d1 --- /dev/null +++ b/homeassistant/components/airos/strings.json @@ -0,0 +1,87 @@ +{ + "config": { + "flow_title": "Ubiquiti airOS device", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "host": "IP address or hostname of the airOS device", + "username": "Administrator username for the airOS device, normally 'ubnt'", + "password": "Password configured through the UISP app or web interface" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "key_data_missing": "Expected data not returned from the device, check the documentation for supported devices", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "host_cpuload": { + "name": "CPU load" + }, + "host_netrole": { + "name": "Network role", + "state": { + "bridge": "Bridge", + "router": "Router" + } + }, + "wireless_frequency": { + "name": "Wireless frequency" + }, + "wireless_essid": { + "name": "Wireless SSID" + }, + "wireless_mode": { + "name": "Wireless mode", + "state": { + "ap_ptp": "Access point", + "sta_ptp": "Station" + } + }, + "wireless_antenna_gain": { + "name": "Antenna gain" + }, + "wireless_throughput_tx": { + "name": "Throughput transmit (actual)" + }, + "wireless_throughput_rx": { + "name": "Throughput receive (actual)" + }, + "wireless_polling_dl_capacity": { + "name": "Download capacity" + }, + "wireless_polling_ul_capacity": { + "name": "Upload capacity" + }, + "wireless_remote_hostname": { + "name": "Remote hostname" + } + } + }, + "exceptions": { + "invalid_auth": { + "message": "[%key:common::config_flow::error::invalid_auth%]" + }, + "cannot_connect": { + "message": "[%key:common::config_flow::error::cannot_connect%]" + }, + "key_data_missing": { + "message": "Key data not returned from device" + }, + "error_data_missing": { + "message": "Data incomplete or missing" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5d468fd1dc904b..5816a0ddbd97b5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -37,6 +37,7 @@ "airgradient", "airly", "airnow", + "airos", "airq", "airthings", "airthings_ble", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a673b05218df5f..5f4ae434074d8d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -7002,6 +7002,12 @@ "ubiquiti": { "name": "Ubiquiti", "integrations": { + "airos": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling", + "name": "Ubiquiti airOS" + }, "unifi": { "integration_type": "hub", "config_flow": true, diff --git a/mypy.ini b/mypy.ini index ba5ac08d3c9792..8482138cc45877 100644 --- a/mypy.ini +++ b/mypy.ini @@ -285,6 +285,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.airos.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.airq.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 1359413cd3a0d6..abb0e9ded9c180 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -452,6 +452,9 @@ airgradient==0.9.2 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airos +airos==0.2.1 + # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31004789f97976..8c544ff5a883de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -434,6 +434,9 @@ airgradient==0.9.2 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airos +airos==0.2.1 + # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/tests/components/airos/__init__.py b/tests/components/airos/__init__.py new file mode 100644 index 00000000000000..8c6182a86506f3 --- /dev/null +++ b/tests/components/airos/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Ubiquity airOS integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/airos/conftest.py b/tests/components/airos/conftest.py new file mode 100644 index 00000000000000..b17908e801aadd --- /dev/null +++ b/tests/components/airos/conftest.py @@ -0,0 +1,61 @@ +"""Common fixtures for the Ubiquiti airOS tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from airos.airos8 import AirOSData +import pytest + +from homeassistant.components.airos.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry, load_json_object_fixture + + +@pytest.fixture +def ap_fixture(): + """Load fixture data for AP mode.""" + json_data = load_json_object_fixture("airos_ap-ptp.json", DOMAIN) + return AirOSData.from_dict(json_data) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.airos.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_airos_client( + request: pytest.FixtureRequest, ap_fixture: AirOSData +) -> Generator[AsyncMock]: + """Fixture to mock the AirOS API client.""" + with ( + patch( + "homeassistant.components.airos.config_flow.AirOS", autospec=True + ) as mock_airos, + patch("homeassistant.components.airos.coordinator.AirOS", new=mock_airos), + patch("homeassistant.components.airos.AirOS", new=mock_airos), + ): + client = mock_airos.return_value + client.status.return_value = ap_fixture + client.login.return_value = True + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the AirOS mocked config entry.""" + return MockConfigEntry( + title="NanoStation", + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + CONF_USERNAME: "ubnt", + }, + unique_id="01:23:45:67:89:AB", + ) diff --git a/tests/components/airos/fixtures/airos_ap-ptp.json b/tests/components/airos/fixtures/airos_ap-ptp.json new file mode 100644 index 00000000000000..06d13ba11015b6 --- /dev/null +++ b/tests/components/airos/fixtures/airos_ap-ptp.json @@ -0,0 +1,300 @@ +{ + "chain_names": [ + { "number": 1, "name": "Chain 0" }, + { "number": 2, "name": "Chain 1" } + ], + "host": { + "hostname": "NanoStation 5AC ap name", + "device_id": "03aa0d0b40fed0a47088293584ef5432", + "uptime": 264888, + "power_time": 268683, + "time": "2025-06-23 23:06:42", + "timestamp": 2668313184, + "fwversion": "v8.7.17", + "devmodel": "NanoStation 5AC loco", + "netrole": "bridge", + "loadavg": 0.412598, + "totalram": 63447040, + "freeram": 16564224, + "temperature": 0, + "cpuload": 10.10101, + "height": 3 + }, + "genuine": "/images/genuine.png", + "services": { + "dhcpc": false, + "dhcpd": false, + "dhcp6d_stateful": false, + "pppoe": false, + "airview": 2 + }, + "firewall": { + "iptables": false, + "ebtables": false, + "ip6tables": false, + "eb6tables": false + }, + "portfw": false, + "wireless": { + "essid": "DemoSSID", + "mode": "ap-ptp", + "ieeemode": "11ACVHT80", + "band": 2, + "compat_11n": 0, + "hide_essid": 0, + "apmac": "01:23:45:67:89:AB", + "antenna_gain": 13, + "frequency": 5500, + "center1_freq": 5530, + "dfs": 1, + "distance": 0, + "security": "WPA2", + "noisef": -89, + "txpower": -3, + "aprepeater": false, + "rstatus": 5, + "chanbw": 80, + "rx_chainmask": 3, + "tx_chainmask": 3, + "nol_state": 0, + "nol_timeout": 0, + "cac_state": 0, + "cac_timeout": 0, + "rx_idx": 8, + "rx_nss": 2, + "tx_idx": 9, + "tx_nss": 2, + "throughput": { "tx": 222, "rx": 9907 }, + "service": { "time": 267181, "link": 266003 }, + "polling": { + "cb_capacity": 593970, + "dl_capacity": 647400, + "ul_capacity": 540540, + "use": 48, + "tx_use": 6, + "rx_use": 42, + "atpc_status": 2, + "fixed_frame": false, + "gps_sync": false, + "ff_cap_rep": false + }, + "count": 1, + "sta": [ + { + "mac": "01:23:45:67:89:AB", + "lastip": "192.168.1.2", + "signal": -59, + "rssi": 37, + "noisefloor": -89, + "chainrssi": [35, 32, 0], + "tx_idx": 9, + "rx_idx": 8, + "tx_nss": 2, + "rx_nss": 2, + "tx_latency": 0, + "distance": 1, + "tx_packets": 0, + "tx_lretries": 0, + "tx_sretries": 0, + "uptime": 170281, + "dl_signal_expect": -80, + "ul_signal_expect": -55, + "cb_capacity_expect": 416000, + "dl_capacity_expect": 208000, + "ul_capacity_expect": 624000, + "dl_rate_expect": 3, + "ul_rate_expect": 8, + "dl_linkscore": 100, + "ul_linkscore": 86, + "dl_avg_linkscore": 100, + "ul_avg_linkscore": 88, + "tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430], + "stats": { + "rx_bytes": 206938324814, + "rx_packets": 149767200, + "rx_pps": 846, + "tx_bytes": 5265602739, + "tx_packets": 52980390, + "tx_pps": 0 + }, + "airmax": { + "actual_priority": 0, + "beam": 0, + "desired_priority": 0, + "cb_capacity": 593970, + "dl_capacity": 647400, + "ul_capacity": 540540, + "atpc_status": 2, + "rx": { + "usage": 42, + "cinr": 31, + "evm": [ + [ + 31, 28, 33, 32, 32, 32, 31, 31, 31, 29, 30, 32, 30, 27, 34, 31, + 31, 30, 32, 29, 31, 29, 31, 33, 31, 31, 32, 30, 31, 34, 33, 31, + 30, 31, 30, 31, 31, 32, 31, 30, 33, 31, 30, 31, 27, 31, 30, 30, + 30, 30, 30, 29, 32, 34, 31, 30, 28, 30, 29, 35, 31, 33, 32, 29 + ], + [ + 34, 34, 35, 34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, + 34, 34, 35, 34, 33, 33, 35, 34, 34, 35, 34, 35, 34, 34, 35, 34, + 34, 33, 34, 34, 34, 34, 34, 35, 35, 35, 34, 35, 33, 34, 34, 34, + 34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35 + ] + ] + }, + "tx": { + "usage": 6, + "cinr": 31, + "evm": [ + [ + 32, 34, 28, 33, 35, 30, 31, 33, 30, 30, 32, 30, 29, 33, 31, 29, + 33, 31, 31, 30, 33, 34, 33, 31, 33, 32, 32, 31, 29, 31, 30, 32, + 31, 30, 29, 32, 31, 32, 31, 31, 32, 29, 31, 29, 30, 32, 32, 31, + 32, 32, 33, 31, 28, 29, 31, 31, 33, 32, 33, 32, 32, 32, 31, 33 + ], + [ + 37, 37, 37, 38, 38, 37, 36, 38, 38, 37, 37, 37, 37, 37, 39, 37, + 37, 37, 37, 37, 37, 36, 37, 37, 37, 37, 37, 37, 37, 38, 37, 37, + 38, 37, 37, 37, 38, 37, 38, 37, 37, 37, 37, 37, 36, 37, 37, 37, + 37, 37, 37, 38, 37, 37, 38, 37, 36, 37, 37, 37, 37, 37, 37, 37 + ] + ] + } + }, + "last_disc": 1, + "remote": { + "age": 1, + "device_id": "d4f4cdf82961e619328a8f72f8d7653b", + "hostname": "NanoStation 5AC sta name", + "platform": "NanoStation 5AC loco", + "version": "WA.ar934x.v8.7.17.48152.250620.2132", + "time": "2025-06-23 23:13:54", + "cpuload": 43.564301, + "temperature": 0, + "totalram": 63447040, + "freeram": 14290944, + "netrole": "bridge", + "mode": "sta-ptp", + "sys_id": "0xe7fa", + "tx_throughput": 16023, + "rx_throughput": 251, + "uptime": 265320, + "power_time": 268512, + "compat_11n": 0, + "signal": -58, + "rssi": 38, + "noisefloor": -90, + "tx_power": -4, + "distance": 1, + "rx_chainmask": 3, + "chainrssi": [33, 37, 0], + "tx_ratedata": [ + 14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154 + ], + "tx_bytes": 212308148210, + "rx_bytes": 3624206478, + "antenna_gain": 13, + "cable_loss": 0, + "height": 2, + "ethlist": [ + { + "ifname": "eth0", + "enabled": true, + "plugged": true, + "duplex": true, + "speed": 1000, + "snr": [30, 30, 29, 30], + "cable_len": 14 + } + ], + "ipaddr": ["192.168.1.2"], + "ip6addr": ["fe80::eea:14ff:fea4:89ab"], + "gps": { "lat": "52.379894", "lon": "4.901608", "fix": 0 }, + "oob": false, + "unms": { "status": 0, "timestamp": null }, + "airview": 2, + "service": { "time": 267195, "link": 265996 } + }, + "airos_connected": true + } + ], + "sta_disconnected": [] + }, + "interfaces": [ + { + "ifname": "eth0", + "hwaddr": "01:23:45:67:89:AB", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": true, + "tx_bytes": 209900085624, + "rx_bytes": 3984971949, + "tx_packets": 185866883, + "rx_packets": 73564835, + "tx_errors": 0, + "rx_errors": 4, + "tx_dropped": 10, + "rx_dropped": 0, + "ipaddr": "0.0.0.0", + "speed": 1000, + "duplex": true, + "snr": [30, 30, 30, 30], + "cable_len": 18, + "ip6addr": null + } + }, + { + "ifname": "ath0", + "hwaddr": "01:23:45:67:89:AB", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": false, + "tx_bytes": 5265602738, + "rx_bytes": 206938324766, + "tx_packets": 52980390, + "rx_packets": 149767200, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 2005, + "rx_dropped": 0, + "ipaddr": "0.0.0.0", + "speed": 0, + "duplex": false, + "snr": null, + "cable_len": null, + "ip6addr": null + } + }, + { + "ifname": "br0", + "hwaddr": "01:23:45:67:89:AB", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": true, + "tx_bytes": 236295176, + "rx_bytes": 204802727, + "tx_packets": 298119, + "rx_packets": 1791592, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 0, + "rx_dropped": 0, + "ipaddr": "192.168.1.2", + "speed": 0, + "duplex": false, + "snr": null, + "cable_len": null, + "ip6addr": [{ "addr": "fe80::eea:14ff:fea4:89cd", "plen": 64 }] + } + } + ], + "provmode": {}, + "ntpclient": {}, + "unms": { "status": 0, "timestamp": null }, + "gps": { "lat": 52.379894, "lon": 4.901608, "fix": 0 }, + "derived": { "mac": "01:23:45:67:89:AB", "mac_interface": "br0" } +} diff --git a/tests/components/airos/snapshots/test_sensor.ambr b/tests/components/airos/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..a92d2dc35a214e --- /dev/null +++ b/tests/components/airos/snapshots/test_sensor.ambr @@ -0,0 +1,547 @@ +# serializer version: 1 +# name: test_all_entities[sensor.nanostation_5ac_ap_name_antenna_gain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.nanostation_5ac_ap_name_antenna_gain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Antenna gain', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_antenna_gain', + 'unique_id': '01:23:45:67:89:AB_wireless_antenna_gain', + 'unit_of_measurement': 'dB', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_antenna_gain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'NanoStation 5AC ap name Antenna gain', + 'state_class': , + 'unit_of_measurement': 'dB', + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_antenna_gain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_cpu_load-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_cpu_load', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CPU load', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'host_cpuload', + 'unique_id': '01:23:45:67:89:AB_host_cpuload', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_cpu_load-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NanoStation 5AC ap name CPU load', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_cpu_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.10101', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_download_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.nanostation_5ac_ap_name_download_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Download capacity', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_polling_dl_capacity', + 'unique_id': '01:23:45:67:89:AB_wireless_polling_dl_capacity', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_download_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'NanoStation 5AC ap name Download capacity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_download_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '647400', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_network_role-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'bridge', + 'router', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_network_role', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Network role', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'host_netrole', + 'unique_id': '01:23:45:67:89:AB_host_netrole', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_network_role-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'NanoStation 5AC ap name Network role', + 'options': list([ + 'bridge', + 'router', + ]), + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_network_role', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'bridge', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_receive_actual-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.nanostation_5ac_ap_name_throughput_receive_actual', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Throughput receive (actual)', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_throughput_rx', + 'unique_id': '01:23:45:67:89:AB_wireless_throughput_rx', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_receive_actual-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'NanoStation 5AC ap name Throughput receive (actual)', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_throughput_receive_actual', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9907', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_transmit_actual-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.nanostation_5ac_ap_name_throughput_transmit_actual', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Throughput transmit (actual)', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_throughput_tx', + 'unique_id': '01:23:45:67:89:AB_wireless_throughput_tx', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_transmit_actual-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'NanoStation 5AC ap name Throughput transmit (actual)', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_throughput_transmit_actual', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '222', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_upload_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.nanostation_5ac_ap_name_upload_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upload capacity', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_polling_ul_capacity', + 'unique_id': '01:23:45:67:89:AB_wireless_polling_ul_capacity', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_upload_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'NanoStation 5AC ap name Upload capacity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_upload_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '540540', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_frequency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wireless frequency', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_frequency', + 'unique_id': '01:23:45:67:89:AB_wireless_frequency', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'NanoStation 5AC ap name Wireless frequency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5500', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ap_ptp', + 'sta_ptp', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wireless mode', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_mode', + 'unique_id': '01:23:45:67:89:AB_wireless_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'NanoStation 5AC ap name Wireless mode', + 'options': list([ + 'ap_ptp', + 'sta_ptp', + ]), + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ap_ptp', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_ssid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wireless SSID', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_essid', + 'unique_id': '01:23:45:67:89:AB_wireless_essid', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_ssid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NanoStation 5AC ap name Wireless SSID', + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'DemoSSID', + }) +# --- diff --git a/tests/components/airos/test_config_flow.py b/tests/components/airos/test_config_flow.py new file mode 100644 index 00000000000000..9d2a6376732670 --- /dev/null +++ b/tests/components/airos/test_config_flow.py @@ -0,0 +1,119 @@ +"""Test the Ubiquiti airOS config flow.""" + +from typing import Any +from unittest.mock import AsyncMock + +from airos.exceptions import ( + ConnectionAuthenticationError, + DeviceConnectionError, + KeyDataMissingError, +) +import pytest + +from homeassistant.components.airos.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +MOCK_CONFIG = { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "ubnt", + CONF_PASSWORD: "test-password", +} + + +async def test_form_creates_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_airos_client: AsyncMock, + ap_fixture: dict[str, Any], +) -> None: + """Test we get the form and create the appropriate entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "NanoStation 5AC ap name" + assert result["result"].unique_id == "01:23:45:67:89:AB" + assert result["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_duplicate_entry( + hass: HomeAssistant, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test the form does not allow duplicate entries.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (ConnectionAuthenticationError, "invalid_auth"), + (DeviceConnectionError, "cannot_connect"), + (KeyDataMissingError, "key_data_missing"), + (Exception, "unknown"), + ], +) +async def test_form_exception_handling( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_airos_client: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test we handle exceptions.""" + mock_airos_client.login.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_airos_client.login.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "NanoStation 5AC ap name" + assert result["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/airos/test_sensor.py b/tests/components/airos/test_sensor.py new file mode 100644 index 00000000000000..561741b1a2bfe0 --- /dev/null +++ b/tests/components/airos/test_sensor.py @@ -0,0 +1,85 @@ +"""Test the Ubiquiti airOS sensors.""" + +from datetime import timedelta +from unittest.mock import AsyncMock + +from airos.exceptions import ( + ConnectionAuthenticationError, + DataMissingError, + DeviceConnectionError, +) +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.airos.const import SCAN_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("exception"), + [ + ConnectionAuthenticationError, + TimeoutError, + DeviceConnectionError, + DataMissingError, + ], +) +async def test_sensor_update_exception_handling( + hass: HomeAssistant, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + freezer: FrozenDateTimeFactory, +) -> None: + """Test entity update data handles exceptions.""" + await setup_integration(hass, mock_config_entry) + + expected_entity_id = "sensor.nanostation_5ac_ap_name_antenna_gain" + signal_state = hass.states.get(expected_entity_id) + + assert signal_state.state == "13", f"Expected state 13, got {signal_state.state}" + assert signal_state.attributes.get("unit_of_measurement") == "dB", ( + f"Expected unit 'dB', got {signal_state.attributes.get('unit_of_measurement')}" + ) + + mock_airos_client.login.side_effect = exception + + freezer.tick(timedelta(seconds=SCAN_INTERVAL.total_seconds() + 1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + signal_state = hass.states.get(expected_entity_id) + + assert signal_state.state == STATE_UNAVAILABLE, ( + f"Expected state {STATE_UNAVAILABLE}, got {signal_state.state}" + ) + + mock_airos_client.login.side_effect = None + + freezer.tick(timedelta(seconds=SCAN_INTERVAL.total_seconds())) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + signal_state = hass.states.get(expected_entity_id) + assert signal_state.state == "13", f"Expected state 13, got {signal_state.state}" From aaec243bf49ccaaf73a7ebe730ab46c1dbff2aa7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Jul 2025 07:49:20 -1000 Subject: [PATCH 93/96] Properly cleanup ONVIF events to prevent log flooding on setup errors (#149603) --- homeassistant/components/onvif/__init__.py | 105 +++++++++++---------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 057993be1814eb..83dc238d2c48d6 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,7 +1,7 @@ """The ONVIF integration.""" import asyncio -from contextlib import suppress +from contextlib import AsyncExitStack, suppress from http import HTTPStatus import logging @@ -45,50 +45,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device = ONVIFDevice(hass, entry) - try: - await device.async_setup() - if not entry.data.get(CONF_SNAPSHOT_AUTH): - await async_populate_snapshot_auth(hass, device, entry) - except (TimeoutError, aiohttp.ClientError) as err: - await device.device.close() - raise ConfigEntryNotReady( - f"Could not connect to camera {device.device.host}:{device.device.port}: {err}" - ) from err - except Fault as err: - await device.device.close() - if is_auth_error(err): - raise ConfigEntryAuthFailed( - f"Auth Failed: {stringify_onvif_error(err)}" + async with AsyncExitStack() as stack: + # Register cleanup callback for device + @stack.push_async_callback + async def _cleanup(): + await _async_stop_device(hass, device) + + try: + await device.async_setup() + if not entry.data.get(CONF_SNAPSHOT_AUTH): + await async_populate_snapshot_auth(hass, device, entry) + except (TimeoutError, aiohttp.ClientError) as err: + raise ConfigEntryNotReady( + f"Could not connect to camera {device.device.host}:{device.device.port}: {err}" ) from err - raise ConfigEntryNotReady( - f"Could not connect to camera: {stringify_onvif_error(err)}" - ) from err - except ONVIFError as err: - await device.device.close() - raise ConfigEntryNotReady( - f"Could not setup camera {device.device.host}:{device.device.port}: {stringify_onvif_error(err)}" - ) from err - except TransportError as err: - await device.device.close() - stringified_onvif_error = stringify_onvif_error(err) - if err.status_code in ( - HTTPStatus.UNAUTHORIZED.value, - HTTPStatus.FORBIDDEN.value, - ): - raise ConfigEntryAuthFailed( - f"Auth Failed: {stringified_onvif_error}" + except Fault as err: + if is_auth_error(err): + raise ConfigEntryAuthFailed( + f"Auth Failed: {stringify_onvif_error(err)}" + ) from err + raise ConfigEntryNotReady( + f"Could not connect to camera: {stringify_onvif_error(err)}" + ) from err + except ONVIFError as err: + raise ConfigEntryNotReady( + f"Could not setup camera {device.device.host}:{device.device.port}: {stringify_onvif_error(err)}" + ) from err + except TransportError as err: + stringified_onvif_error = stringify_onvif_error(err) + if err.status_code in ( + HTTPStatus.UNAUTHORIZED.value, + HTTPStatus.FORBIDDEN.value, + ): + raise ConfigEntryAuthFailed( + f"Auth Failed: {stringified_onvif_error}" + ) from err + raise ConfigEntryNotReady( + f"Could not setup camera {device.device.host}:{device.device.port}: {stringified_onvif_error}" ) from err - raise ConfigEntryNotReady( - f"Could not setup camera {device.device.host}:{device.device.port}: {stringified_onvif_error}" - ) from err - except asyncio.CancelledError as err: - # After https://github.com/agronholm/anyio/issues/374 is resolved - # this may be able to be removed - await device.device.close() - raise ConfigEntryNotReady(f"Setup was unexpectedly canceled: {err}") from err - - if not device.available: - raise ConfigEntryNotReady + except asyncio.CancelledError as err: + # After https://github.com/agronholm/anyio/issues/374 is resolved + # this may be able to be removed + raise ConfigEntryNotReady( + f"Setup was unexpectedly canceled: {err}" + ) from err + + if not device.available: + raise ConfigEntryNotReady + + # If we get here, setup was successful - prevent cleanup + stack.pop_all() hass.data[DOMAIN][entry.unique_id] = device @@ -111,17 +117,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - - device: ONVIFDevice = hass.data[DOMAIN][entry.unique_id] - +async def _async_stop_device(hass: HomeAssistant, device: ONVIFDevice) -> None: + """Stop the ONVIF device.""" if device.capabilities.events and device.events.started: try: await device.events.async_stop() except (TimeoutError, ONVIFError, Fault, aiohttp.ClientError, TransportError): LOGGER.warning("Error while stopping events: %s", device.name) + await device.device.close() + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + device: ONVIFDevice = hass.data[DOMAIN][entry.unique_id] + await _async_stop_device(hass, device) return await hass.config_entries.async_unload_platforms(entry, device.platforms) From 9f45801409644a60d7011578c43c3345ba3952c0 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:03:27 -0700 Subject: [PATCH 94/96] Remove advanced mode from group `all` option. (#149626) --- homeassistant/components/group/config_flow.py | 4 +--- tests/components/group/test_config_flow.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 5e36087e9e4f07..152e629be2ed20 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -141,9 +141,7 @@ async def light_switch_options_schema( """Generate options schema.""" return (await basic_group_options_schema(domain, handler)).extend( { - vol.Required( - CONF_ALL, default=False, description={"advanced": True} - ): selector.BooleanSelector(), + vol.Required(CONF_ALL, default=False): selector.BooleanSelector(), } ) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 30adae2fd2a3bd..322e6ebdad0e53 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -315,11 +315,11 @@ async def test_options( ("group_type", "extra_options", "extra_options_after", "advanced"), [ ("light", {"all": False}, {"all": False}, False), - ("light", {"all": True}, {"all": True}, False), + ("light", {"all": True}, {"all": False}, False), ("light", {"all": False}, {"all": False}, True), ("light", {"all": True}, {"all": False}, True), ("switch", {"all": False}, {"all": False}, False), - ("switch", {"all": True}, {"all": True}, False), + ("switch", {"all": True}, {"all": False}, False), ("switch", {"all": False}, {"all": False}, True), ("switch", {"all": True}, {"all": False}, True), ], From c4c4463c63ffe11f25c8d90a06d93c15c7190132 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 29 Jul 2025 23:00:49 +0300 Subject: [PATCH 95/96] Update IQS for Alexa Devices (#149639) --- homeassistant/components/alexa_devices/quality_scale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alexa_devices/quality_scale.yaml b/homeassistant/components/alexa_devices/quality_scale.yaml index 47ff53dd04ea2b..95433655212b22 100644 --- a/homeassistant/components/alexa_devices/quality_scale.yaml +++ b/homeassistant/components/alexa_devices/quality_scale.yaml @@ -48,7 +48,7 @@ rules: comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration docs-data-update: done docs-examples: done - docs-known-limitations: todo + docs-known-limitations: done docs-supported-devices: done docs-supported-functions: done docs-troubleshooting: done From 62713b137162553b64d0a9255aa6b6c5ce0dfc4d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:32:32 +0200 Subject: [PATCH 96/96] Update pyblu to 2.0.4 (#149589) --- homeassistant/components/bluesound/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index caf5cc7541df0b..54fb061676d610 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bluesound", "iot_class": "local_polling", - "requirements": ["pyblu==2.0.1"], + "requirements": ["pyblu==2.0.4"], "zeroconf": [ { "type": "_musc._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index abb0e9ded9c180..91cfb6e023614f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1870,7 +1870,7 @@ pybbox==0.0.5-alpha pyblackbird==0.6 # homeassistant.components.bluesound -pyblu==2.0.1 +pyblu==2.0.4 # homeassistant.components.neato pybotvac==0.0.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c544ff5a883de..2ba7f3af443deb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1572,7 +1572,7 @@ pybalboa==1.1.3 pyblackbird==0.6 # homeassistant.components.bluesound -pyblu==2.0.1 +pyblu==2.0.4 # homeassistant.components.neato pybotvac==0.0.28