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 001/265] 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 00000000000..f0f243ca386 --- /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 558a3d668ae..3f835b5d82b 100644 --- a/homeassistant/components/remote_calendar/config_flow.py +++ b/homeassistant/components/remote_calendar/config_flow.py @@ -4,13 +4,14 @@ from http import HTTPStatus 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 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN): 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 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN): 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 1eead7682d3..26876b53224 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.core import HomeAssistant 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 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]): super().__init__( hass, _LOGGER, - name=DOMAIN, + name=f"{DOMAIN}_{config_entry.title}", update_interval=SCAN_INTERVAL, always_update=True, ) @@ -46,13 +47,19 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]): 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 ef7f20d4699..48ef6080bdb 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 9aff1594db3..9bea46ab27e 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 f4ca500b2e1..d3e6b439805 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 002/265] 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 dbde1ee1ef3..e2b9f8631d8 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -124,6 +124,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): 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 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): 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 d63e5a7ae2a..dd6b21ab5e5 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 003/265] 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 4fb5f57320f..2cbc962a305 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 04b3f0eaa2f..4a4454e9dab 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 ea925412def..c40ea6e17fc 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 44fb913489d..35eb320893f 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 004/265] 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 dd255c57c13..d13b180d62d 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 4a4454e9dab..2fece9385aa 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 c40ea6e17fc..6118a3ede0b 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 005/265] 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 e29491e2b21..4b28a1029a4 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 b748aad241c..3a210856391 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -248,8 +248,6 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): 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 2fece9385aa..e67b8687e5d 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 6118a3ede0b..ca114781c23 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 006/265] 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 70cf6a2c072..0d44d57ac5e 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 f0fa428fbb3..e666f06c5f2 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 c34a268e347..77d788a1dd0 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 1791d12268b..011d76e66b5 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 e67b8687e5d..dd663763a54 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 ca114781c23..f624e1d552b 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 007/265] 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 a3feb120460..c57648c9551 100644 --- a/homeassistant/components/telegram_bot/bot.py +++ b/homeassistant/components/telegram_bot/bot.py @@ -374,9 +374,7 @@ class TelegramNotificationService: } 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 @@ class TelegramNotificationService: 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 41f26ccd48d..8d3d9b0cd7b 100644 --- a/homeassistant/components/telegram_bot/config_flow.py +++ b/homeassistant/components/telegram_bot/config_flow.py @@ -159,8 +159,6 @@ class OptionsFlowHandler(OptionsFlow): """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 d5fc0e134d5..b1d94d381ac 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 2586761b584..9a076016a32 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 6590bbed1cf..73dd9e27763 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -50,6 +50,7 @@ from homeassistant.components.telegram_bot.const import ( 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 008/265] 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 5f74da954a0..8387ecc9c9f 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 dd663763a54..fb548289de5 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 f624e1d552b..e3b58a5dfe8 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 009/265] 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 eee6cb85e6d..f43d89aa098 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 010/265] 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 7c23edd92ce..70281390436 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 fb548289de5..cae697cdfe9 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 e3b58a5dfe8..de092158c7e 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 011/265] 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 bfd868a5334..748d8f0c6f0 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 e666f06c5f2..824c3d945fe 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 cae697cdfe9..04ed2500e01 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 de092158c7e..fede2337353 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 012/265] [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 19cc8bd3af7..c5491df0021 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 71f3d54bef6..7e55191639b 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -64,7 +64,7 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N """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 013/265] 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 f3045fe49e8..5991dc8fe51 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 04ed2500e01..fcd97e305fc 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 fede2337353..a234822689c 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 014/265] 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 f1a9f7f79c2..258a9c9a48f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 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 77d788a1dd0..b4478a69d53 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 015/265] 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 03df289a2fd..f09c2e3f2a7 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 016/265] 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 f09c2e3f2a7..bab4f90c6d1 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 017/265] 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 9a37cc554c7..9340573f6ce 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 fcd97e305fc..3a612431369 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 a234822689c..e284b9dbb18 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 018/265] 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 731d1ffe9c3..3ce70f00c0d 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -140,7 +140,14 @@ class RestData: 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 c688ff1b314..cbc77e9d53f 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 eea3f4e88b4..a3c714e1f62 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -191,7 +191,6 @@ class AiohttpClientMockResponse: if response is None: response = b"" - self.charset = "utf-8" self.method = method self._url = url self.status = status @@ -261,16 +260,32 @@ class AiohttpClientMockResponse: """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 019/265] 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 3ce70f00c0d..7f346277314 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -49,7 +49,7 @@ class RestData: # 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 315f8113309..af7503a7007 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 a3c714e1f62..c3a8be77b77 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -156,6 +156,9 @@ class AiohttpClientMocker: 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 020/265] 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 7fdafc4dda1..10cb23146ae 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 3a612431369..bac56ca7d70 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 e284b9dbb18..107b8d21703 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 021/265] 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 9f9009693e5..c29fc582462 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 bac56ca7d70..3bc9d412cc5 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 107b8d21703..5c9ae5c6235 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 022/265] 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 a82da20396f..eca520d8946 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 3bc9d412cc5..c886f94a68a 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 5c9ae5c6235..51510f46a33 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 023/265] 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 80c8fb7f8f2..2af38cb3d17 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -98,7 +98,10 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): 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 8ac470ae922..c3c3e9a564f 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 c886f94a68a..7458c0f94e6 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 51510f46a33..13a04de9064 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 564ff9afa9b..2445140aff4 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -4,7 +4,12 @@ from aiowebostv import WebOsTvPairError 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 024/265] 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 8deb2eee414..ba87890de03 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 7458c0f94e6..8c4850e3162 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 13a04de9064..c75aca5c8e4 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 49388428805..a4dc0a39be6 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 c53d1c78f6e..1fb46e9a4d8 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 db0cf2ff849..0fe387d3126 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 16cd824a489..167e4db3aee 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 890edc00482..4095bf8bf53 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 025/265] 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 7f346277314..3341f296fb9 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -115,6 +115,16 @@ class RestData: 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 cbc77e9d53f..b830d6b7743 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 @@ from homeassistant.const import ( 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 026/265] 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 8387ecc9c9f..278045001fc 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 8c4850e3162..a45d470b0a6 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 c75aca5c8e4..1c61f8c7811 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 027/265] 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 99dc7a09f87..3106aba5e49 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -18,6 +18,11 @@ from .entity import SmartThingsEntity 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 7dd57e89c6a..de4587b7ca8 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 028/265] 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 5a424ea7892..f93df234289 100644 --- a/homeassistant/components/switchbot_cloud/sensor.py +++ b/homeassistant/components/switchbot_cloud/sensor.py @@ -113,11 +113,11 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = { ), "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 029/265] 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 8e727425a2a..5ef7eec9976 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 a45d470b0a6..b5777776929 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 1c61f8c7811..0496d7bdd9a 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 030/265] 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 284e4d83569..d49862180bd 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 2daf3fc48ff..452244f05b5 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 15cd9859d6e..e5ce32b2293 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -24,6 +24,8 @@ MOCK_ENTRY_DATA = { "connection_type": "nibegw", } +MOCK_UNIQUE_ID = "mock_entry_unique_id" + class MockConnection(Connection): """A mock connection class.""" @@ -59,7 +61,9 @@ class MockConnection(Connection): 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 00000000000..37dd7a8679c --- /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 00000000000..01f35bd8a54 --- /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 00000000000..30010ac61c4 --- /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 5015bba4092..4f2bab7ad0a 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 a9620b5ddb3..85e932f8018 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 dc7faf0a80e..e054979b7a9 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 00000000000..4221de52ba1 --- /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 031/265] 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 70281390436..34fdd1448a5 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 b5777776929..dada15791d2 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 0496d7bdd9a..38da863b93a 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 79851550528..a5a49a343a9 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 032/265] 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 ff977438f38..2d68b3be345 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.aiohttp_client import async_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 8cb2e4dfc61..e352e337d0b 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -33,7 +33,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -83,7 +83,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): **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 033/265] 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 4fc2c0b05df..afbb779b696 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -66,7 +66,7 @@ ENTITIES: tuple[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 10cb23146ae..3c070769b5b 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 dada15791d2..d17b1030922 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 38da863b93a..6ad4aa867bf 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 034/265] 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 7c3fc47e512..2c4974a6567 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 d17b1030922..b15b6207ac0 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 6ad4aa867bf..087be4dc645 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 035/265] 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 47e2a01e798..8243a55d779 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 b15b6207ac0..c7cb2d62836 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 087be4dc645..9c8acdcd535 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 036/265] 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 0d44d57ac5e..7c64100873c 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 824c3d945fe..6c6eac1f6dd 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 b4478a69d53..4b9140296a7 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 011d76e66b5..61aeaf81d14 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 c7cb2d62836..5218bb9068a 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 9c8acdcd535..aabad61a45d 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 037/265] 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 66a819f1e8d..121238df9fe 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 192ddffd330..4d9bbeb9516 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -22,14 +22,11 @@ from homeassistant.const import ( 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 62882c7df8b..56ad9fdcb0e 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 038/265] 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 338779f32cb..f1594a7b034 100644 --- a/homeassistant/components/mqtt/entity.py +++ b/homeassistant/components/mqtt/entity.py @@ -389,16 +389,6 @@ def async_setup_entity_entry_helper( _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 @@ class MqttEntity( 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 35a9a0494a6..04b4bda0d79 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 039/265] 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 80dcd87cd88..906356a4bc9 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 9955e355c96..e0af5c1c67f 100644 --- a/homeassistant/components/immich/update.py +++ b/homeassistant/components/immich/update.py @@ -44,7 +44,7 @@ class ImmichUpdateEntity(ImmichEntity, UpdateEntity): 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 5218bb9068a..b431371da33 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 aabad61a45d..5f5dd955b63 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 040/265] 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 8a37ebf63e4..a95af62da6c 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -171,14 +171,19 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat 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 61de0c99940..794d6ac4397 100644 --- a/tests/components/fritzbox/test_coordinator.py +++ b/tests/components/fritzbox/test_coordinator.py @@ -15,7 +15,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow -from . import FritzDeviceCoverMock, FritzDeviceSwitchMock, 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 041/265] 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 3c9d33424a8..76faaefa931 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -41,7 +41,12 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady 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 @@ class HomeConnectCoordinator( 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 ee4d5f1d729..e61ec5e2b1f 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 042/265] 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 76faaefa931..bb419f6bd7c 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -655,10 +655,7 @@ class HomeConnectCoordinator( "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 99c89ec8788..e1c0b42ca0b 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 043/265] 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 c6a4206de4a..32eb1aae2b0 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -178,6 +178,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) 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 97ef29dfaca..6ba3da8b867 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 044/265] 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 474ab640943..c82cad012c3 100644 --- a/homeassistant/components/renson/fan.py +++ b/homeassistant/components/renson/fan.py @@ -196,7 +196,7 @@ class RensonFan(RensonEntity, FanEntity): 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 045/265] 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 d3c7dbba12b..191cd9a0454 100644 --- a/homeassistant/components/miele/config_flow.py +++ b/homeassistant/components/miele/config_flow.py @@ -26,14 +26,6 @@ class OAuth2FlowHandler( """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 bbe5844c1cd..5ce129b255d 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 046/265] 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 7e55191639b..71f3d54bef6 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -64,7 +64,7 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N """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 6c6eac1f6dd..9a02b999775 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 4b9140296a7..17fb74f452c 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 61aeaf81d14..67c38d690b1 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 047/265] 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 748d8f0c6f0..a7582ebc5e2 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 9a02b999775..01bf8e24885 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 b431371da33..02520158cd4 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 5f5dd955b63..374fc8cc36b 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 048/265] 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 cd5055383ea..8a548cde8bb 100644 --- a/homeassistant/components/google_cloud/stt.py +++ b/homeassistant/components/google_cloud/stt.py @@ -127,7 +127,7 @@ class GoogleCloudSpeechToTextEntity(SpeechToTextEntity): 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 16519645dee..817c424d1fc 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -218,7 +218,7 @@ class BaseGoogleCloudProvider: 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 049/265] 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 7088b624e21..5f789813869 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -505,8 +505,13 @@ class ClimateCapabilities(AlexaEntity): ): 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 @@ class FanCapabilities(AlexaEntity): 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 @@ class RemoteCapabilities(AlexaEntity): 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 @@ class HumidifierCapabilities(AlexaEntity): """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 6998b2acc97..4d8d0dca67f 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -5,6 +5,7 @@ from unittest.mock import patch 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 050/265] 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 3c91db5b86d..c4eaddcc1fe 100644 --- a/homeassistant/components/snoo/binary_sensor.py +++ b/homeassistant/components/snoo/binary_sensor.py @@ -38,7 +38,7 @@ BINARY_SENSOR_DESCRIPTIONS: list[SnooBinarySensorEntityDescription] = [ 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 051/265] 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 | 45 ++++++++------- .../nyt_games/snapshots/test_sensor.ambr | 8 +-- 6 files changed, 61 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/nyt_games/manifest.json b/homeassistant/components/nyt_games/manifest.json index c32de754782..db3ad6a85f1 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 02520158cd4..3eb2312b079 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 374fc8cc36b..c7e94225cf0 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 73a6f440fc0..16601243052 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 ad4d8e2e416..d35ce4cdebc 100644 --- a/tests/components/nyt_games/fixtures/new_account.json +++ b/tests/components/nyt_games/fixtures/new_account.json @@ -7,26 +7,6 @@ "stats": { "wordle": { "legacyStats": { - "gamesPlayed": 1, - "gamesWon": 1, - "guesses": { - "1": 0, - "2": 0, - "3": 0, - "4": 0, - "5": 1, - "6": 0, - "fail": 0 - }, - "currentStreak": 0, - "maxStreak": 1, - "lastWonDayOffset": 1118, - "hasPlayed": true, - "autoOptInTimestamp": 1727357874700, - "hasMadeStatsChoice": false, - "timestamp": 1727358123 - }, - "calculatedStats": { "gamesPlayed": 0, "gamesWon": 0, "guesses": { @@ -38,12 +18,35 @@ "6": 0, "fail": 0 }, + "currentStreak": 0, + "maxStreak": 1, + "lastWonDayOffset": 1118, + "hasPlayed": true, + "autoOptInTimestamp": 1727357874700, + "hasMadeStatsChoice": false, + "timestamp": 1727358123 + }, + "calculatedStats": { "currentStreak": 0, "maxStreak": 1, "lastWonPrintDate": "", "lastCompletedPrintDate": "", + "hasPlayed": false + }, + "totalStats": { + "gamesPlayed": 1, + "gamesWon": 1, + "guesses": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 1, + "6": 0, + "fail": 0 + }, "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 5a1aa384f0f..10fddcfa365 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 052/265] 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 439df76c838..6905cefdc30 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -125,8 +125,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( key="charge_state_conn_charge_cable", polling=True, polling_value_fn=lambda x: x != "", - streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType( - lambda value: callback(value 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 053/265] 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 fed777e6435..8e632e46efe 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 3eb2312b079..8361bf5cefd 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 c7e94225cf0..6cf669bb013 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 054/265] 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 ee451b5f81d..a3cf2d1d12f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -2114,6 +2114,9 @@ def data_schema_from_fields( 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 9386f1da32c..77c74001939 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 055/265] 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 1db8dbf55c6..08c9163bb3b 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 8361bf5cefd..c3b3239101e 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 6cf669bb013..b34163d2ee8 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 056/265] 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 34fdd1448a5..0cb99ba090e 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 c3b3239101e..c7d125c0700 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 b34163d2ee8..a388e6dbf87 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 057/265] 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 258a9c9a48f..9dd3c3480f9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 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 17fb74f452c..47c38246f29 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 058/265] 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 c42ebff200d..d66594da390 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -113,9 +113,7 @@ class HomematicipHAP: 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 @@ class HomematicipHAP: """ 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 @@ class HomematicipHAP: 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 @@ class HomematicipHAP: 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 @@ class HomematicipHAP: """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 @@ class HomematicipHAP: 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 d5af2859873..036ffa286a3 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 c7d125c0700..4d9efd17d23 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 a388e6dbf87..dd15dfca16a 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 abd0e18b368..48ad8806d0c 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 ae094f7dded..69078beafaf 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 059/265] 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 3a6f5f221c5..cefcbb86a98 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -868,8 +868,8 @@ RPC_SENSORS: Final = { 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 93351e9bc65..4895766cc49 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 0b8ec71771b..9dcda321057 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 060/265] 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 2c4974a6567..35354570f23 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 4d9efd17d23..e69675779db 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 dd15dfca16a..533573e2a36 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 061/265] 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 4c92e0bd222..cf86fbeb4f9 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 f58783e04a4..d12cf278d59 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 c0cbc2ea431..26f26990d58 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 e69675779db..61e344b9f3e 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 533573e2a36..554905e48fd 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 062/265] 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 4e15b93330c..8d3e988dd14 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -306,6 +306,11 @@ class WebRTCProvider(CameraWebRTCProvider): 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 2abdf724f61..dcbcb629d11 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 063/265] 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 f8b3dbbe492..f3471d01da1 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -72,7 +72,7 @@ class SonosFavorites(SonosHouseholdCoordinator): """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 064/265] 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 cf1d6157ec1..12772b894b6 100644 --- a/homeassistant/components/teslemetry/button.py +++ b/homeassistant/components/teslemetry/button.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant 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 065/265] 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 278045001fc..320179bf2df 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 61e344b9f3e..6451b711663 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 554905e48fd..70748d6c5fe 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 066/265] 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 ba87890de03..1782320a357 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 6451b711663..ca5075fd995 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 70748d6c5fe..2e0f0a0d96c 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 067/265] 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 1782320a357..8c6765ece89 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 ca5075fd995..967535ca0b7 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 2e0f0a0d96c..337ed2e849d 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 068/265] 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 119d1d31d52..eac8ddcf713 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 0289d5100d6..4a73bf779e0 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 a2ab8e6e466..1b927757a39 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 93943b0a9ea..2471e45b4e0 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 62ee4ede7d9..825c5774c1d 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 07970cb25ca..d65ebb3a25a 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 01bf8e24885..98124be5c64 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 967535ca0b7..4b3048742a9 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 337ed2e849d..6e0659ce614 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 069/265] 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 0b96b354436..27239a5a520 100644 --- a/homeassistant/components/syncthru/coordinator.py +++ b/homeassistant/components/syncthru/coordinator.py @@ -28,6 +28,7 @@ class SyncthruCoordinator(DataUpdateCoordinator[SyncThru]): 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 070/265] 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 a7582ebc5e2..791acf8a39c 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 98124be5c64..e783e8e0743 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 4b3048742a9..c65bc509fa0 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 6e0659ce614..560071f9644 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 071/265] 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 5ef7eec9976..22168c21f97 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 c65bc509fa0..af8161e4636 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 560071f9644..e0b95a9c4ce 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 072/265] 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 783a0b30b14..83679894d71 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 997c014cd13..16f0c9f22bc 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 073/265] 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 0cb99ba090e..6904f8000ce 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 af8161e4636..c9f708ad9ab 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 e0b95a9c4ce..2e0ad947c38 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 074/265] 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 9dd3c3480f9..62e6d49befd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 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 47c38246f29..c010ecb7254 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 075/265] 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 d1ab9009308..08a587d8d20 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -5,7 +5,6 @@ from __future__ import annotations 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 .discovery import ZwaveDiscoveryInfo 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 @@ class ZWaveBaseEntity(Entity): ) ) - 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 @@ class ZWaveBaseEntity(Entity): @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 985c4a86813..bf32754f50d 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -200,18 +200,13 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) 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 4350d7f7649..324a0f14941 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -37,7 +37,11 @@ from homeassistant.helpers import ( ) 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 1011026ac68..9e36810872f 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -28,7 +28,7 @@ from homeassistant.components.zwave_js.lock import ( 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 fc225d529a6..17f154f4f78 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 076/265] 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 f1ab0489b86..1b2772a9c80 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 b6f100280ad..e7255d478cb 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 52d092ed9f0..2326734ceaf 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 9185768165a..9dbcf09a7d4 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 c9f708ad9ab..56a1e7f8597 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 2e0ad947c38..bac418c6d59 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 077/265] 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 f9cd751a81e..e986cc302ae 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 01e0c4458e4..520f8578c6e 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 078/265] 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 0bfad34681a..29c3305858b 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -82,7 +82,7 @@ class PushBot(BaseTelegramBot): 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 @@ class PushBot(BaseTelegramBot): 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 @@ class PushBotView(HomeAssistantView): 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 @@ class PushBotView(HomeAssistantView): 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 73dd9e27763..80b9859ceab 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 3419d33074d..a02bb3e3358 100644 --- a/tests/components/telegram_bot/test_webhooks.py +++ b/tests/components/telegram_bot/test_webhooks.py @@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, patch 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 079/265] 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 893c30dfd41..c5b91cefd2e 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 56a1e7f8597..0b6fa6d9685 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 bac418c6d59..0917f77dc27 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 080/265] 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 c61fd0879fa..a86938730c9 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -5,6 +5,7 @@ from __future__ import annotations 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 @@ class MatterLight(MatterEntity, LightEntity): 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 @@ class MatterLight(MatterEntity, LightEntity): 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 b600ededa6e..f9abf986170 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 081/265] 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 3d30fcd369e..7a1b51ac8ae 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -10,6 +10,9 @@ import aiolifx_effects as aiolifx_effects_module 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 @@ class LIFXLight(LIFXEntity, LightEntity): 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 d66908c1b1a..edb13c259e8 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -30,6 +30,8 @@ from homeassistant.components.lifx.manager import ( 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 082/265] 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 7e95e274713..925946ed7f1 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -495,10 +495,23 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): 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 7445182e5f6..835dfbf7471 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 a1642746d03..c708b1c9d66 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 test_usb_discovery_migration( 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 test_usb_discovery_migration_restore_driver_ready_timeout( 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 083/265] 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 bf1df07823c..85ef119a583 100644 --- a/homeassistant/components/fujitsu_fglair/climate.py +++ b/homeassistant/components/fujitsu_fglair/climate.py @@ -15,6 +15,7 @@ from homeassistant.components.climate import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, + FAN_OFF, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, @@ -31,6 +32,7 @@ from .coordinator import FGLairConfigEntry, FGLairCoordinator 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 084/265] 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 d73234b1fdd..761bbebf7a8 100644 --- a/homeassistant/components/tesla_fleet/const.py +++ b/homeassistant/components/tesla_fleet/const.py @@ -14,9 +14,8 @@ CONF_REFRESH_TOKEN = "refresh_token" 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 c51cd83ee66..a43ec14fc51 100644 --- a/tests/components/tesla_fleet/__init__.py +++ b/tests/components/tesla_fleet/__init__.py @@ -8,7 +8,7 @@ from homeassistant.components.application_credentials import ( 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 085/265] 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 d77d70ff86c..d128e3e5111 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 968f925d1e8..c9829746d59 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -144,7 +144,8 @@ async def async_send_message( # noqa: C901 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 @@ async def async_send_message( # noqa: C901 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 0b6fa6d9685..dbc6d309c15 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 086/265] 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 6904f8000ce..c1eb3369c11 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 dbc6d309c15..78aa0cc4f6c 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 0917f77dc27..f4c0f06175f 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 087/265] 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 9149f216563..5c23240ce91 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 78aa0cc4f6c..be200069c50 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 f4c0f06175f..e978a39f58b 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 9d29191289e..005c14b7458 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 088/265] 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 8b03e5efe01..d890ed23676 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 be200069c50..6eb24ec184b 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 e978a39f58b..ebd3f9e863b 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 089/265] 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 62e6d49befd..6db2d238a98 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 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 c010ecb7254..03ad9af59a1 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 090/265] 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 59775655854..abcf32f2659 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -17,6 +17,7 @@ from . import bootstrap 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 00000000000..41982df9e50 --- /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 00000000000..a32ceb1062c --- /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 091/265] 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 a43eadce0de..24c107e5611 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 d15a93fd8bd..35a2bf2c7fb 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 6110854f5f6..a332eb930c2 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 092/265] 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 c6e27a011f1..c125e85bbfc 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 4e7c1b9175a..5ef8479d4d3 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 8b64cffaa7e..bb345775a60 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 00000000000..54f0db205a9 --- /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 00000000000..287f54101c8 --- /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 00000000000..f4be2594613 --- /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 00000000000..3f0f1a12380 --- /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 00000000000..e54962110fc --- /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 00000000000..cb6119a6fa9 --- /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 00000000000..a0bacd5ebba --- /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 00000000000..690bf21fc8e --- /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 00000000000..6823ba8520b --- /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 5d468fd1dc9..5816a0ddbd9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -37,6 +37,7 @@ FLOWS = { "airgradient", "airly", "airnow", + "airos", "airq", "airthings", "airthings_ble", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a673b05218d..5f4ae434074 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 ba5ac08d3c9..8482138cc45 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 1359413cd3a..abb0e9ded9c 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 31004789f97..8c544ff5a88 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 00000000000..8c6182a8650 --- /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 00000000000..b17908e801a --- /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 00000000000..06d13ba1101 --- /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 00000000000..a92d2dc35a2 --- /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 00000000000..9d2a6376732 --- /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 00000000000..561741b1a2b --- /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 093/265] Properly cleanup ONVIF events to prevent log flooding on setup errors (#149603) --- homeassistant/components/onvif/__init__.py | 107 +++++++++++---------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 057993be181..83dc238d2c4 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)}" - ) 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}" - ) 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 + async with AsyncExitStack() as stack: + # Register cleanup callback for device + @stack.push_async_callback + async def _cleanup(): + await _async_stop_device(hass, device) - if not device.available: - raise ConfigEntryNotReady + 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 + 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 + 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 094/265] 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 5e36087e9e4..152e629be2e 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 30adae2fd2a..322e6ebdad0 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 095/265] 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 47ff53dd04e..95433655212 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 096/265] 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 caf5cc7541d..54fb061676d 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 abb0e9ded9c..91cfb6e0236 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 8c544ff5a88..2ba7f3af443 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 From 52ee5d53ee68ac5cafe6d98fa3e602d58924057d Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:27:43 +0200 Subject: [PATCH 097/265] bump pyenphase to 2.2.3 (#149641) --- 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 320179bf2df..e337dac74e0 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.2"], + "requirements": ["pyenphase==2.2.3"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 91cfb6e0236..835128628ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1966,7 +1966,7 @@ pyegps==0.2.5 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==2.2.2 +pyenphase==2.2.3 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ba7f3af443..56eaec04d3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1641,7 +1641,7 @@ pyegps==0.2.5 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==2.2.2 +pyenphase==2.2.3 # homeassistant.components.everlights pyeverlights==0.1.0 From 73e578b168b5c5d9de952db9bc9f2f4a36993e69 Mon Sep 17 00:00:00 2001 From: hypnosiss <11396064+hypnosiss@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:29:53 +0200 Subject: [PATCH 098/265] Bump pymysensors library version (#149632) --- homeassistant/components/mysensors/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index a4b802f001c..f9cabda90b7 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mysensors", "iot_class": "local_push", "loggers": ["mysensors"], - "requirements": ["pymysensors==0.25.0"] + "requirements": ["pymysensors==0.26.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 835128628ae..1e2f4ec081e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2164,7 +2164,7 @@ pymonoprice==0.4 pymsteams==0.1.12 # homeassistant.components.mysensors -pymysensors==0.25.0 +pymysensors==0.26.0 # homeassistant.components.iron_os pynecil==4.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56eaec04d3e..d42453d82fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1800,7 +1800,7 @@ pymodbus==3.9.2 pymonoprice==0.4 # homeassistant.components.mysensors -pymysensors==0.25.0 +pymysensors==0.26.0 # homeassistant.components.iron_os pynecil==4.1.1 From 45ae34cc0e7822db732d5aef19c9a42d31221774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 30 Jul 2025 00:23:03 +0200 Subject: [PATCH 099/265] Strip leading and trailing whitespace in program names in miele action response (#149643) --- homeassistant/components/miele/services.py | 2 +- tests/components/miele/fixtures/programs.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/miele/services.py b/homeassistant/components/miele/services.py index 6d4dc77dd36..3d73c021b3d 100644 --- a/homeassistant/components/miele/services.py +++ b/homeassistant/components/miele/services.py @@ -126,7 +126,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse: "programs": [ { "program_id": item["programId"], - "program": item["program"], + "program": item["program"].strip(), "parameters": ( { "temperature": ( diff --git a/tests/components/miele/fixtures/programs.json b/tests/components/miele/fixtures/programs.json index 06eddc5fedc..ce2348f61de 100644 --- a/tests/components/miele/fixtures/programs.json +++ b/tests/components/miele/fixtures/programs.json @@ -11,7 +11,7 @@ }, { "programId": 123, - "program": "Dark garments / Denim", + "program": "Dark garments / Denim ", "parameters": {} }, { From 0dd1e0cabbd8e5a8282b19caab3dd32512cc8ddb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 30 Jul 2025 09:06:15 +0200 Subject: [PATCH 100/265] Suppress exception stack trace when writing MQTT entity state if a ValueError occured (#149583) --- homeassistant/components/mqtt/models.py | 9 +++++++++ tests/components/mqtt/test_init.py | 27 +++++++++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 8a42797b0f2..4cc0424195a 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -364,6 +364,15 @@ class EntityTopicState: entity_id, entity = self.subscribe_calls.popitem() try: entity.async_write_ha_state() + except ValueError as exc: + _LOGGER.error( + "Value error while updating state of %s, topic: " + "'%s' with payload: %s: %s", + entity_id, + msg.topic, + msg.payload, + exc, + ) except Exception: _LOGGER.exception( "Exception raised while updating state of %s, topic: " diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index f789d7f3be1..1aeb9843b54 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -604,6 +604,23 @@ def test_entity_device_info_schema() -> None: ) +@pytest.mark.parametrize( + ("side_effect", "error_message"), + [ + ( + ValueError("Invalid value for sensor"), + "Value error while updating " + "state of sensor.test_sensor, topic: 'test/state' " + "with payload: b'payload causing errors'", + ), + ( + TypeError("Invalid value for sensor"), + "Exception raised while updating " + "state of sensor.test_sensor, topic: 'test/state' " + "with payload: b'payload causing errors'", + ), + ], +) @pytest.mark.parametrize( "hass_config", [ @@ -625,6 +642,8 @@ async def test_handle_logging_on_writing_the_entity_state( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, + side_effect: Exception, + error_message: str, ) -> None: """Test on log handling when an error occurs writing the state.""" await mqtt_mock_entry() @@ -637,7 +656,7 @@ async def test_handle_logging_on_writing_the_entity_state( assert state.state == "initial_state" with patch( "homeassistant.helpers.entity.Entity.async_write_ha_state", - side_effect=ValueError("Invalid value for sensor"), + side_effect=side_effect, ): async_fire_mqtt_message(hass, "test/state", b"payload causing errors") await hass.async_block_till_done() @@ -645,11 +664,7 @@ async def test_handle_logging_on_writing_the_entity_state( assert state is not None assert state.state == "initial_state" assert "Invalid value for sensor" in caplog.text - assert ( - "Exception raised while updating " - "state of sensor.test_sensor, topic: 'test/state' " - "with payload: b'payload causing errors'" in caplog.text - ) + assert error_message in caplog.text async def test_receiving_non_utf8_message_gets_logged( From 2ee82e1d6f548d04e5dcfd768b42ce5189e2e9a3 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 30 Jul 2025 09:24:16 +0200 Subject: [PATCH 101/265] Remove battery attribute from Ecovacs vacuums (#149581) --- homeassistant/components/ecovacs/vacuum.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index d432410c8c5..86a30558375 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any from deebot_client.capabilities import Capabilities, DeviceType from deebot_client.device import Device -from deebot_client.events import BatteryEvent, FanSpeedEvent, RoomsEvent, StateEvent +from deebot_client.events import FanSpeedEvent, RoomsEvent, StateEvent from deebot_client.models import CleanAction, CleanMode, Room, State import sucks @@ -216,7 +216,6 @@ class EcovacsVacuum( VacuumEntityFeature.PAUSE | VacuumEntityFeature.STOP | VacuumEntityFeature.RETURN_HOME - | VacuumEntityFeature.BATTERY | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.LOCATE | VacuumEntityFeature.STATE @@ -243,10 +242,6 @@ class EcovacsVacuum( """Set up the event listeners now that hass is ready.""" await super().async_added_to_hass() - async def on_battery(event: BatteryEvent) -> None: - self._attr_battery_level = event.value - self.async_write_ha_state() - async def on_rooms(event: RoomsEvent) -> None: self._rooms = event.rooms self.async_write_ha_state() @@ -255,7 +250,6 @@ class EcovacsVacuum( self._attr_activity = _STATE_TO_VACUUM_STATE[event.state] self.async_write_ha_state() - self._subscribe(self._capability.battery.event, on_battery) self._subscribe(self._capability.state.event, on_status) if self._capability.fan_speed: From f66e83f33ebe382696edc612c3580c2c3b156942 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 30 Jul 2025 09:54:00 +0200 Subject: [PATCH 102/265] Add dynamic encryption key support to the ESPHome integration (#148746) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .../components/esphome/config_flow.py | 30 +- .../esphome/encryption_key_storage.py | 94 ++++ homeassistant/components/esphome/manager.py | 98 +++- tests/components/esphome/test_config_flow.py | 115 +++++ .../esphome/test_dynamic_encryption.py | 102 ++++ tests/components/esphome/test_manager.py | 484 +++++++++++++++++- 6 files changed, 918 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/esphome/encryption_key_storage.py create mode 100644 tests/components/esphome/test_dynamic_encryption.py diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 75408246e78..dc0e9b8e1b1 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -51,6 +51,7 @@ from .const import ( DOMAIN, ) from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info +from .encryption_key_storage import async_get_encryption_key_storage from .entry_data import ESPHomeConfigEntry from .manager import async_replace_device @@ -159,7 +160,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle reauthorization flow.""" errors = {} - if await self._retrieve_encryption_key_from_dashboard(): + if ( + await self._retrieve_encryption_key_from_storage() + or await self._retrieve_encryption_key_from_dashboard() + ): error = await self.fetch_device_info() if error is None: return await self._async_authenticate_or_add() @@ -226,9 +230,12 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): response = await self.fetch_device_info() self._noise_psk = None + # Try to retrieve an existing key from dashboard or storage. if ( self._device_name and await self._retrieve_encryption_key_from_dashboard() + ) or ( + self._device_mac and await self._retrieve_encryption_key_from_storage() ): response = await self.fetch_device_info() @@ -284,6 +291,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = discovery_info.properties.get("friendly_name", device_name) self._host = discovery_info.host self._port = discovery_info.port + self._device_mac = mac_address self._noise_required = bool(discovery_info.properties.get("api_encryption")) # Check if already configured @@ -772,6 +780,26 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._noise_psk = noise_psk return True + async def _retrieve_encryption_key_from_storage(self) -> bool: + """Try to retrieve the encryption key from storage. + + Return boolean if a key was retrieved. + """ + # Try to get MAC address from current flow state or reauth entry + mac_address = self._device_mac + if mac_address is None and self._reauth_entry is not None: + # In reauth flow, get MAC from the existing entry's unique_id + mac_address = self._reauth_entry.unique_id + + assert mac_address is not None + + storage = await async_get_encryption_key_storage(self.hass) + if stored_key := await storage.async_get_key(mac_address): + self._noise_psk = stored_key + return True + + return False + @staticmethod @callback def async_get_options_flow( diff --git a/homeassistant/components/esphome/encryption_key_storage.py b/homeassistant/components/esphome/encryption_key_storage.py new file mode 100644 index 00000000000..e4b5ef41c2e --- /dev/null +++ b/homeassistant/components/esphome/encryption_key_storage.py @@ -0,0 +1,94 @@ +"""Encryption key storage for ESPHome devices.""" + +from __future__ import annotations + +import logging +from typing import TypedDict + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.singleton import singleton +from homeassistant.helpers.storage import Store +from homeassistant.util.hass_dict import HassKey + +_LOGGER = logging.getLogger(__name__) + +ENCRYPTION_KEY_STORAGE_VERSION = 1 +ENCRYPTION_KEY_STORAGE_KEY = "esphome.encryption_keys" + + +class EncryptionKeyData(TypedDict): + """Encryption key storage data.""" + + keys: dict[str, str] # MAC address -> base64 encoded key + + +KEY_ENCRYPTION_STORAGE: HassKey[ESPHomeEncryptionKeyStorage] = HassKey( + "esphome_encryption_key_storage" +) + + +class ESPHomeEncryptionKeyStorage: + """Storage for ESPHome encryption keys.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the encryption key storage.""" + self.hass = hass + self._store = Store[EncryptionKeyData]( + hass, + ENCRYPTION_KEY_STORAGE_VERSION, + ENCRYPTION_KEY_STORAGE_KEY, + encoder=JSONEncoder, + ) + self._data: EncryptionKeyData | None = None + + async def async_load(self) -> None: + """Load encryption keys from storage.""" + if self._data is None: + data = await self._store.async_load() + self._data = data or {"keys": {}} + + async def async_save(self) -> None: + """Save encryption keys to storage.""" + if self._data is not None: + await self._store.async_save(self._data) + + async def async_get_key(self, mac_address: str) -> str | None: + """Get encryption key for a MAC address.""" + await self.async_load() + assert self._data is not None + return self._data["keys"].get(mac_address.lower()) + + async def async_store_key(self, mac_address: str, key: str) -> None: + """Store encryption key for a MAC address.""" + await self.async_load() + assert self._data is not None + self._data["keys"][mac_address.lower()] = key + await self.async_save() + _LOGGER.debug( + "Stored encryption key for device with MAC %s", + mac_address, + ) + + async def async_remove_key(self, mac_address: str) -> None: + """Remove encryption key for a MAC address.""" + await self.async_load() + assert self._data is not None + lower_mac_address = mac_address.lower() + if lower_mac_address in self._data["keys"]: + del self._data["keys"][lower_mac_address] + await self.async_save() + _LOGGER.debug( + "Removed encryption key for device with MAC %s", + mac_address, + ) + + +@singleton(KEY_ENCRYPTION_STORAGE, async_=True) +async def async_get_encryption_key_storage( + hass: HomeAssistant, +) -> ESPHomeEncryptionKeyStorage: + """Get the encryption key storage instance.""" + storage = ESPHomeEncryptionKeyStorage(hass) + await storage.async_load() + return storage diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 5e9e11171af..4d5de77b1e0 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -3,8 +3,10 @@ from __future__ import annotations import asyncio +import base64 from functools import partial import logging +import secrets from typing import TYPE_CHECKING, Any, NamedTuple from aioesphomeapi import ( @@ -68,6 +70,7 @@ from .const import ( CONF_ALLOW_SERVICE_CALLS, CONF_BLUETOOTH_MAC_ADDRESS, CONF_DEVICE_NAME, + CONF_NOISE_PSK, CONF_SUBSCRIBE_LOGS, DEFAULT_ALLOW_SERVICE_CALLS, DEFAULT_URL, @@ -78,6 +81,7 @@ from .const import ( ) from .dashboard import async_get_dashboard from .domain_data import DomainData +from .encryption_key_storage import async_get_encryption_key_storage # Import config flow so that it's added to the registry from .entry_data import ESPHomeConfigEntry, RuntimeEntryData @@ -85,9 +89,7 @@ from .entry_data import ESPHomeConfigEntry, RuntimeEntryData DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}" if TYPE_CHECKING: - from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined] - SubscribeLogsResponse, - ) + from aioesphomeapi.api_pb2 import SubscribeLogsResponse # type: ignore[attr-defined] # noqa: I001 _LOGGER = logging.getLogger(__name__) @@ -515,6 +517,8 @@ class ESPHomeManager: assert api_version is not None, "API version must be set" entry_data.async_on_connect(device_info, api_version) + await self._handle_dynamic_encryption_key(device_info) + if device_info.name: reconnect_logic.name = device_info.name @@ -618,6 +622,7 @@ class ESPHomeManager: ), ): return + if isinstance(err, InvalidEncryptionKeyAPIError): if ( (received_name := err.received_name) @@ -648,6 +653,93 @@ class ESPHomeManager: return self.entry.async_start_reauth(self.hass) + async def _handle_dynamic_encryption_key( + self, device_info: EsphomeDeviceInfo + ) -> None: + """Handle dynamic encryption keys. + + If a device reports it supports encryption, but we connected without a key, + we need to generate and store one. + """ + noise_psk: str | None = self.entry.data.get(CONF_NOISE_PSK) + if noise_psk: + # we're already connected with a noise PSK - nothing to do + return + + if not device_info.api_encryption_supported: + # device does not support encryption - nothing to do + return + + # Connected to device without key and the device supports encryption + storage = await async_get_encryption_key_storage(self.hass) + + # First check if we have a key in storage for this device + from_storage: bool = False + if self.entry.unique_id and ( + stored_key := await storage.async_get_key(self.entry.unique_id) + ): + _LOGGER.debug( + "Retrieved encryption key from storage for device %s", + self.entry.unique_id, + ) + # Use the stored key + new_key = stored_key.encode() + new_key_str = stored_key + from_storage = True + else: + # No stored key found, generate a new one + _LOGGER.debug( + "Generating new encryption key for device %s", self.entry.unique_id + ) + new_key = base64.b64encode(secrets.token_bytes(32)) + new_key_str = new_key.decode() + + try: + # Store the key on the device using the existing connection + result = await self.cli.noise_encryption_set_key(new_key) + except APIConnectionError as ex: + _LOGGER.error( + "Connection error while storing encryption key for device %s (%s): %s", + self.entry.data.get(CONF_DEVICE_NAME, self.host), + self.entry.unique_id, + ex, + ) + return + else: + if not result: + _LOGGER.error( + "Failed to set dynamic encryption key on device %s (%s)", + self.entry.data.get(CONF_DEVICE_NAME, self.host), + self.entry.unique_id, + ) + return + + # Key stored successfully on device + assert self.entry.unique_id is not None + + # Only store in storage if it was newly generated + if not from_storage: + await storage.async_store_key(self.entry.unique_id, new_key_str) + + # Always update config entry + self.hass.config_entries.async_update_entry( + self.entry, + data={**self.entry.data, CONF_NOISE_PSK: new_key_str}, + ) + + if from_storage: + _LOGGER.info( + "Set encryption key from storage on device %s (%s)", + self.entry.data.get(CONF_DEVICE_NAME, self.host), + self.entry.unique_id, + ) + else: + _LOGGER.info( + "Generated and stored encryption key for device %s (%s)", + self.entry.data.get(CONF_DEVICE_NAME, self.host), + self.entry.unique_id, + ) + @callback def _async_handle_logging_changed(self, _event: Event) -> None: """Handle when the logging level changes.""" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 3f0148262e4..d76991a984c 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -27,6 +27,9 @@ from homeassistant.components.esphome.const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) +from homeassistant.components.esphome.encryption_key_storage import ( + ENCRYPTION_KEY_STORAGE_KEY, +) from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant @@ -41,6 +44,118 @@ from .conftest import MockGenericDeviceEntryType from tests.common import MockConfigEntry + +async def test_retrieve_encryption_key_from_storage_with_device_mac( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], +) -> None: + """Test key successfully retrieved from storage.""" + + # Mock the encryption key storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": {"keys": {"11:22:33:44:55:aa": VALID_NOISE_PSK}}, + } + + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test", "11:22:33:44:55:AA"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_reauth_fixed_from_from_storage( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], +) -> None: + """Test reauth fixed automatically via storage.""" + + # Mock the encryption key storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": {"keys": {"11:22:33:44:55:aa": VALID_NOISE_PSK}}, + } + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo( + uses_password=False, name="test", mac_address="11:22:33:44:55:aa" + ) + + result = await entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + +async def test_retrieve_encryption_key_from_storage_no_key_found( + hass: HomeAssistant, + mock_client: APIClient, +) -> None: + """Test _retrieve_encryption_key_from_storage when no key is found.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo( + uses_password=False, name="test", mac_address="11:22:33:44:55:aa" + ) + + result = await entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM, result + assert result["step_id"] == "reauth_confirm" + assert CONF_NOISE_PSK not in entry.data + + INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM=" WRONG_NOISE_PSK = "GP+ciK+nVfTQ/gcz6uOdS+oKEdJgesU+jeu8Ssj2how=" diff --git a/tests/components/esphome/test_dynamic_encryption.py b/tests/components/esphome/test_dynamic_encryption.py new file mode 100644 index 00000000000..cbdcc35aea2 --- /dev/null +++ b/tests/components/esphome/test_dynamic_encryption.py @@ -0,0 +1,102 @@ +"""Tests for ESPHome dynamic encryption key generation.""" + +from __future__ import annotations + +import base64 + +from homeassistant.components.esphome.encryption_key_storage import ( + ESPHomeEncryptionKeyStorage, + async_get_encryption_key_storage, +) +from homeassistant.core import HomeAssistant + + +async def test_dynamic_encryption_key_generation_mock(hass: HomeAssistant) -> None: + """Test that encryption key generation works with mocked storage.""" + storage = await async_get_encryption_key_storage(hass) + + # Store a key + mac_address = "11:22:33:44:55:aa" + test_key = base64.b64encode(b"test_key_32_bytes_long_exactly!").decode() + + await storage.async_store_key(mac_address, test_key) + + # Retrieve a key + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key == test_key + + +async def test_encryption_key_storage_remove_key(hass: HomeAssistant) -> None: + """Test ESPHomeEncryptionKeyStorage async_remove_key method.""" + # Create storage instance + storage = ESPHomeEncryptionKeyStorage(hass) + + # Test removing a key that exists + mac_address = "11:22:33:44:55:aa" + test_key = "test_encryption_key_32_bytes_long" + + # First store a key + await storage.async_store_key(mac_address, test_key) + + # Verify key exists + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key == test_key + + # Remove the key + await storage.async_remove_key(mac_address) + + # Verify key no longer exists + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key is None + + # Test removing a key that doesn't exist (should not raise an error) + non_existent_mac = "aa:bb:cc:dd:ee:ff" + await storage.async_remove_key(non_existent_mac) # Should not raise + + # Test case insensitive removal + upper_mac = "22:33:44:55:66:77" + await storage.async_store_key(upper_mac, test_key) + + # Remove using lowercase MAC address + await storage.async_remove_key(upper_mac.lower()) + + # Verify key was removed + retrieved_key = await storage.async_get_key(upper_mac) + assert retrieved_key is None + + +async def test_encryption_key_basic_storage( + hass: HomeAssistant, +) -> None: + """Test basic encryption key storage functionality.""" + storage = await async_get_encryption_key_storage(hass) + mac_address = "11:22:33:44:55:aa" + key = "test_encryption_key_32_bytes_long" + + # Store key + await storage.async_store_key(mac_address, key) + + # Retrieve key + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key == key + + +async def test_retrieve_key_from_storage( + hass: HomeAssistant, +) -> None: + """Test config flow can retrieve encryption key from storage for new device.""" + # Test that the encryption key storage integration works with config flow + storage = await async_get_encryption_key_storage(hass) + mac_address = "11:22:33:44:55:aa" + stored_key = "test_encryption_key_32_bytes_long" + + # Store encryption key for a device + await storage.async_store_key(mac_address, stored_key) + + # Verify the key can be retrieved (simulating config flow behavior) + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key == stored_key + + # Test case insensitive retrieval (since config flows might use different case) + retrieved_key_upper = await storage.async_get_key(mac_address.upper()) + assert retrieved_key_upper == stored_key diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 318ccde221f..8d2dd211869 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1,8 +1,10 @@ """Test ESPHome manager.""" import asyncio +import base64 import logging -from unittest.mock import AsyncMock, Mock, call +from typing import Any +from unittest.mock import AsyncMock, Mock, call, patch from aioesphomeapi import ( APIClient, @@ -27,11 +29,15 @@ from homeassistant.components.esphome.const import ( CONF_ALLOW_SERVICE_CALLS, CONF_BLUETOOTH_MAC_ADDRESS, CONF_DEVICE_NAME, + CONF_NOISE_PSK, CONF_SUBSCRIBE_LOGS, DOMAIN, STABLE_BLE_URL_VERSION, STABLE_BLE_VERSION_STR, ) +from homeassistant.components.esphome.encryption_key_storage import ( + ENCRYPTION_KEY_STORAGE_KEY, +) from homeassistant.components.esphome.manager import DEVICE_CONFLICT_ISSUE_FORMAT from homeassistant.components.tag import DOMAIN as TAG_DOMAIN from homeassistant.const import ( @@ -1788,3 +1794,479 @@ async def test_sub_device_references_main_device_area( ) assert sub_device_3 is not None assert sub_device_3.suggested_area == "Bedroom" + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_dynamic_encryption_key_generation( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test that a device without a key in storage gets a new one generated.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + expected_key = base64.b64encode(test_key_bytes).decode() + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods + mock_client.noise_encryption_set_key = AsyncMock(return_value=True) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify the key was generated and set + mock_token_bytes.assert_called_once_with(32) + mock_client.noise_encryption_set_key.assert_called_once() + + # Verify config entry was updated + assert entry.data[CONF_NOISE_PSK] == expected_key + + +async def test_manager_retrieves_key_from_storage_on_reconnect( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test that manager retrieves encryption key from storage during reconnect.""" + mac_address = "11:22:33:44:55:aa" + test_key = base64.b64encode(b"existing_key_32_bytes_long!!!").decode() + + # Set up storage with existing key + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": {"keys": {mac_address: test_key}}, + } + + # Create entry without noise PSK (will be loaded from storage) + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods + mock_client.noise_encryption_set_key = AsyncMock(return_value=True) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key retrieval from storage + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify noise_encryption_set_key was called with the stored key + mock_client.noise_encryption_set_key.assert_called_once_with(test_key.encode()) + + # Verify config entry was updated with key from storage + assert entry.data[CONF_NOISE_PSK] == test_key + + +async def test_manager_handle_dynamic_encryption_key_guard_clauses( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test _handle_dynamic_encryption_key guard clauses and early returns.""" + # Test guard clause - no unique_id + entry_no_id = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=None, # No unique ID - should not generate key + ) + entry_no_id.add_to_hass(hass) + + # Set up device without unique ID + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry_no_id, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": "11:22:33:44:55:aa", + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # noise_encryption_set_key should not be called when no unique_id + mock_client.noise_encryption_set_key = AsyncMock() + await device.mock_disconnect(True) + await device.mock_connect() + + mock_client.noise_encryption_set_key.assert_not_called() + + +async def test_manager_handle_dynamic_encryption_key_edge_cases( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test _handle_dynamic_encryption_key edge cases for better coverage.""" + mac_address = "11:22:33:44:55:aa" + + # Test device without encryption support + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Set up device without encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": False, # No encryption support + }, + ) + + # noise_encryption_set_key should not be called when encryption not supported + mock_client.noise_encryption_set_key = AsyncMock() + await device.mock_disconnect(True) + await device.mock_connect() + + mock_client.noise_encryption_set_key.assert_not_called() + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_manager_dynamic_encryption_key_generation_flow( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test the complete dynamic encryption key generation flow.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + expected_key = base64.b64encode(test_key_bytes).decode() + + # Initialize empty storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": { + "keys": {} # No existing keys + }, + } + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods + mock_client.noise_encryption_set_key = AsyncMock(return_value=True) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify the complete flow + mock_token_bytes.assert_called_once_with(32) + mock_client.noise_encryption_set_key.assert_called_once() + assert entry.data[CONF_NOISE_PSK] == expected_key + + # Verify key was stored in hass_storage + assert ( + hass_storage[ENCRYPTION_KEY_STORAGE_KEY]["data"]["keys"][mac_address] + == expected_key + ) + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_manager_handle_dynamic_encryption_key_no_existing_key( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test _handle_dynamic_encryption_key when no existing key is found.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + expected_key = base64.b64encode(test_key_bytes).decode() + + # Initialize empty storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": { + "keys": {} # No existing keys + }, + } + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods + mock_client.noise_encryption_set_key = AsyncMock(return_value=True) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify key generation flow + mock_token_bytes.assert_called_once_with(32) + mock_client.noise_encryption_set_key.assert_called_once() + + # Verify config entry was updated + assert entry.data[CONF_NOISE_PSK] == expected_key + + # Verify key was stored + assert ( + hass_storage[ENCRYPTION_KEY_STORAGE_KEY]["data"]["keys"][mac_address] + == expected_key + ) + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_manager_handle_dynamic_encryption_key_device_set_key_fails( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test _handle_dynamic_encryption_key when noise_encryption_set_key returns False.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + + # Initialize empty storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": { + "keys": {} # No existing keys + }, + } + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods - set_key returns False + mock_client.noise_encryption_set_key = AsyncMock(return_value=False) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Reset mocks since initial connection already happened + mock_token_bytes.reset_mock() + mock_client.noise_encryption_set_key.reset_mock() + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify key generation was attempted with the expected key + mock_token_bytes.assert_called_once_with(32) + mock_client.noise_encryption_set_key.assert_called_once_with( + base64.b64encode(test_key_bytes) + ) + + # Verify config entry was NOT updated since set_key failed + assert CONF_NOISE_PSK not in entry.data + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_manager_handle_dynamic_encryption_key_connection_error( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test _handle_dynamic_encryption_key when noise_encryption_set_key raises APIConnectionError.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + + # Initialize empty storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": { + "keys": {} # No existing keys + }, + } + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods - set_key raises APIConnectionError + mock_client.noise_encryption_set_key = AsyncMock( + side_effect=APIConnectionError("Connection failed") + ) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify key generation was attempted twice (once during setup, once during reconnect) + # This is expected because the first attempt failed with connection error + assert mock_token_bytes.call_count == 2 + mock_token_bytes.assert_called_with(32) + assert mock_client.noise_encryption_set_key.call_count == 2 + + # Verify config entry was NOT updated since connection error occurred + assert CONF_NOISE_PSK not in entry.data + + # Verify key was NOT stored due to connection error + assert mac_address not in hass_storage[ENCRYPTION_KEY_STORAGE_KEY]["data"]["keys"] From 6f8214bbb47364d376cb45794248d11ea307bc74 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 30 Jul 2025 10:22:35 +0200 Subject: [PATCH 103/265] Fix spelling mistakes in abort message of `leaone` (#149653) --- homeassistant/components/leaone/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/leaone/strings.json b/homeassistant/components/leaone/strings.json index bb684941147..53332ce2fec 100644 --- a/homeassistant/components/leaone/strings.json +++ b/homeassistant/components/leaone/strings.json @@ -13,7 +13,7 @@ } }, "abort": { - "no_devices_found": "No supported LeaOne devices found in range; If the device is in range, ensure it has been activated in the last few minutes. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the LeaOne device is present.", + "no_devices_found": "No supported LeaOne devices found in range. If the device is in range, ensure it has been activated in the last few minutes. If you need clarification on whether the device is in range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the LeaOne device is present.", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } From 6b641411a01e036a38e77c3361cbc5dec922753e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:33:09 +0200 Subject: [PATCH 104/265] Bump github/codeql-action from 3.29.4 to 3.29.5 (#149648) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index cc6014b38b0..c5dcf19ce6e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.29.4 + uses: github/codeql-action/init@v3.29.5 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.29.4 + uses: github/codeql-action/analyze@v3.29.5 with: category: "/language:python" From 8e9e304608e3a58d61e8d99164847e193c38a3af Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:38:42 +0200 Subject: [PATCH 105/265] Update lxml to 6.0.0 (#149640) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/requirements.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 28e08372d68..8b9d7ddf37e 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/scrape", "iot_class": "cloud_polling", - "requirements": ["beautifulsoup4==4.13.3", "lxml==5.3.0"] + "requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e2f4ec081e..eafa0b0d47f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1391,7 +1391,7 @@ lupupy==0.3.2 lw12==0.9.2 # homeassistant.components.scrape -lxml==5.3.0 +lxml==6.0.0 # homeassistant.components.matrix matrix-nio==0.25.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d42453d82fe..b4ed33e539b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1189,7 +1189,7 @@ luftdaten==0.7.4 lupupy==0.3.2 # homeassistant.components.scrape -lxml==5.3.0 +lxml==6.0.0 # homeassistant.components.matrix matrix-nio==0.25.2 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 9c3f60a827c..99a1c255e60 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -30,6 +30,7 @@ PACKAGE_CHECK_VERSION_RANGE = { "bleak": "SemVer", "grpcio": "SemVer", "httpx": "SemVer", + "lxml": "SemVer", "mashumaro": "SemVer", "numpy": "SemVer", "pandas": "SemVer", From bb6bcfdd0158a03923751b7e73a21ce132ed75ad Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 30 Jul 2025 11:07:41 +0200 Subject: [PATCH 106/265] Add Z-Wave controller firmware updates (#149623) --- homeassistant/components/zwave_js/__init__.py | 17 +- homeassistant/components/zwave_js/update.py | 128 +++- tests/components/zwave_js/test_update.py | 705 ++++++++++++------ 3 files changed, 580 insertions(+), 270 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index d754419c94c..360969e83d4 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -147,6 +147,7 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +MIN_CONTROLLER_FIRMWARE_SDK_VERSION = AwesomeVersion("6.50.0") PLATFORMS = [ Platform.BINARY_SENSOR, @@ -799,11 +800,19 @@ class NodeEvents: node.on("notification", self.async_on_notification) ) - # Create a firmware update entity for each non-controller device that + # Create a firmware update entity for each device that # supports firmware updates - if not node.is_controller_node and any( - cc.id == CommandClass.FIRMWARE_UPDATE_MD.value - for cc in node.command_classes + controller = self.controller_events.driver_events.driver.controller + if ( + not (is_controller_node := node.is_controller_node) + and any( + cc.id == CommandClass.FIRMWARE_UPDATE_MD.value + for cc in node.command_classes + ) + ) or ( + is_controller_node + and (sdk_version := controller.sdk_version) is not None + and sdk_version >= MIN_CONTROLLER_FIRMWARE_SDK_VERSION ): async_dispatcher_send( self.hass, diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 89fb4dd4aba..42a4b4cf6dd 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -4,26 +4,28 @@ from __future__ import annotations import asyncio from collections import Counter -from collections.abc import Callable +from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Any, Final +from typing import Any, Final, cast from awesomeversion import AwesomeVersion from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver -from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.node.firmware import ( - NodeFirmwareUpdateInfo, - NodeFirmwareUpdateProgress, - NodeFirmwareUpdateResult, +from zwave_js_server.model.firmware import ( + FirmwareUpdateInfo, + FirmwareUpdateProgress, + FirmwareUpdateResult, ) +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.node.firmware import NodeFirmwareUpdateInfo from homeassistant.components.update import ( ATTR_LATEST_VERSION, UpdateDeviceClass, UpdateEntity, + UpdateEntityDescription, UpdateEntityFeature, ) from homeassistant.const import EntityCategory @@ -45,11 +47,54 @@ UPDATE_DELAY_INTERVAL = 5 # In minutes ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware" +@dataclass(frozen=True, kw_only=True) +class ZWaveUpdateEntityDescription(UpdateEntityDescription): + """Class describing Z-Wave update entity.""" + + install_method: Callable[ + [ZWaveFirmwareUpdateEntity, FirmwareUpdateInfo], + Awaitable[FirmwareUpdateResult], + ] + progress_method: Callable[[ZWaveFirmwareUpdateEntity], Callable[[], None]] + finished_method: Callable[[ZWaveFirmwareUpdateEntity], Callable[[], None]] + + +CONTROLLER_UPDATE_ENTITY_DESCRIPTION = ZWaveUpdateEntityDescription( + key="controller_firmware_update", + install_method=( + lambda entity, firmware_update_info: entity.driver.async_firmware_update_otw( + update_info=firmware_update_info + ) + ), + progress_method=lambda entity: entity.driver.on( + "firmware update progress", entity.update_progress + ), + finished_method=lambda entity: entity.driver.on( + "firmware update finished", entity.update_finished + ), +) +NODE_UPDATE_ENTITY_DESCRIPTION = ZWaveUpdateEntityDescription( + key="node_firmware_update", + install_method=( + lambda entity, + firmware_update_info: entity.driver.controller.async_firmware_update_ota( + entity.node, cast(NodeFirmwareUpdateInfo, firmware_update_info) + ) + ), + progress_method=lambda entity: entity.node.on( + "firmware update progress", entity.update_progress + ), + finished_method=lambda entity: entity.node.on( + "firmware update finished", entity.update_finished + ), +) + + @dataclass -class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): +class ZWaveFirmwareUpdateExtraStoredData(ExtraStoredData): """Extra stored data for Z-Wave node firmware update entity.""" - latest_version_firmware: NodeFirmwareUpdateInfo | None + latest_version_firmware: FirmwareUpdateInfo | None def as_dict(self) -> dict[str, Any]: """Return a dict representation of the extra data.""" @@ -60,7 +105,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): } @classmethod - def from_dict(cls, data: dict[str, Any]) -> ZWaveNodeFirmwareUpdateExtraStoredData: + def from_dict(cls, data: dict[str, Any]) -> ZWaveFirmwareUpdateExtraStoredData: """Initialize the extra data from a dict.""" # If there was no firmware info stored, or if it's stale info, we don't restore # anything. @@ -70,7 +115,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): ): return cls(None) - return cls(NodeFirmwareUpdateInfo.from_dict(firmware_dict)) + return cls(FirmwareUpdateInfo.from_dict(firmware_dict)) async def async_setup_entry( @@ -92,7 +137,23 @@ async def async_setup_entry( delay = timedelta(minutes=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL)) driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. - async_add_entities([ZWaveNodeFirmwareUpdate(driver, node, delay)]) + if node.is_controller_node: + # If the node is a controller, we create a controller firmware update entity + entity = ZWaveFirmwareUpdateEntity( + driver, + node, + delay=delay, + entity_description=CONTROLLER_UPDATE_ENTITY_DESCRIPTION, + ) + else: + # If the node is not a controller, we create a node firmware update entity + entity = ZWaveFirmwareUpdateEntity( + driver, + node, + delay=delay, + entity_description=NODE_UPDATE_ENTITY_DESCRIPTION, + ) + async_add_entities([entity]) config_entry.async_on_unload( async_dispatcher_connect( @@ -103,9 +164,12 @@ async def async_setup_entry( ) -class ZWaveNodeFirmwareUpdate(UpdateEntity): +class ZWaveFirmwareUpdateEntity(UpdateEntity): """Representation of a firmware update entity.""" + driver: Driver + entity_description: ZWaveUpdateEntityDescription + node: ZwaveNode _attr_entity_category = EntityCategory.CONFIG _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_supported_features = ( @@ -116,17 +180,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): _attr_has_entity_name = True _attr_should_poll = False - def __init__(self, driver: Driver, node: ZwaveNode, delay: timedelta) -> None: + def __init__( + self, + driver: Driver, + node: ZwaveNode, + delay: timedelta, + entity_description: ZWaveUpdateEntityDescription, + ) -> None: """Initialize a Z-Wave device firmware update entity.""" self.driver = driver + self.entity_description = entity_description self.node = node - self._latest_version_firmware: NodeFirmwareUpdateInfo | None = None + self._latest_version_firmware: FirmwareUpdateInfo | None = None self._status_unsub: Callable[[], None] | None = None self._poll_unsub: Callable[[], None] | None = None self._progress_unsub: Callable[[], None] | None = None self._finished_unsub: Callable[[], None] | None = None self._finished_event = asyncio.Event() - self._result: NodeFirmwareUpdateResult | None = None + self._result: FirmwareUpdateResult | None = None self._delay: Final[timedelta] = delay # Entity class attributes @@ -138,9 +209,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_device_info = get_device_info(driver, node) @property - def extra_restore_state_data(self) -> ZWaveNodeFirmwareUpdateExtraStoredData: + def extra_restore_state_data(self) -> ZWaveFirmwareUpdateExtraStoredData: """Return ZWave Node Firmware Update specific state data to be restored.""" - return ZWaveNodeFirmwareUpdateExtraStoredData(self._latest_version_firmware) + return ZWaveFirmwareUpdateExtraStoredData(self._latest_version_firmware) @callback def _update_on_status_change(self, _: dict[str, Any]) -> None: @@ -149,9 +220,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self.hass.async_create_task(self._async_update()) @callback - def _update_progress(self, event: dict[str, Any]) -> None: + def update_progress(self, event: dict[str, Any]) -> None: """Update install progress on event.""" - progress: NodeFirmwareUpdateProgress = event["firmware_update_progress"] + progress: FirmwareUpdateProgress = event["firmware_update_progress"] if not self._latest_version_firmware: return self._attr_in_progress = True @@ -159,9 +230,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self.async_write_ha_state() @callback - def _update_finished(self, event: dict[str, Any]) -> None: + def update_finished(self, event: dict[str, Any]) -> None: """Update install progress on event.""" - result: NodeFirmwareUpdateResult = event["firmware_update_finished"] + result: FirmwareUpdateResult = event["firmware_update_finished"] self._result = result self._finished_event.set() @@ -266,15 +337,11 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_update_percentage = None self.async_write_ha_state() - self._progress_unsub = self.node.on( - "firmware update progress", self._update_progress - ) - self._finished_unsub = self.node.on( - "firmware update finished", self._update_finished - ) + self._progress_unsub = self.entity_description.progress_method(self) + self._finished_unsub = self.entity_description.finished_method(self) try: - await self.driver.controller.async_firmware_update_ota(self.node, firmware) + await self.entity_description.install_method(self, firmware) except BaseZwaveJSServerError as err: self._unsub_firmware_events_and_reset_progress() raise HomeAssistantError(err) from err @@ -342,8 +409,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): is not None and (extra_data := await self.async_get_last_extra_data()) and ( - latest_version_firmware - := ZWaveNodeFirmwareUpdateExtraStoredData.from_dict( + latest_version_firmware := ZWaveFirmwareUpdateExtraStoredData.from_dict( extra_data.as_dict() ).latest_version_firmware ) diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 17f154f4f78..fbe0a8bbea7 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -1,12 +1,17 @@ """Test the Z-Wave JS update entities.""" import asyncio +from copy import deepcopy from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest from zwave_js_server.event import Event from zwave_js_server.exceptions import FailedZWaveCommand +from zwave_js_server.model.driver.firmware import DriverFirmwareUpdateStatus +from zwave_js_server.model.node import Node from zwave_js_server.model.node.firmware import NodeFirmwareUpdateStatus from homeassistant.components.update import ( @@ -22,11 +27,16 @@ from homeassistant.components.update import ( SERVICE_SKIP, ) from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE -from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util from tests.common import ( @@ -37,7 +47,8 @@ from tests.common import ( ) from tests.typing import WebSocketGenerator -UPDATE_ENTITY = "update.z_wave_thermostat_firmware" +NODE_UPDATE_ENTITY = "update.z_wave_thermostat_firmware" +CONTROLLER_UPDATE_ENTITY = "update.z_stick_gen5_usb_controller_firmware" LATEST_VERSION_FIRMWARE = { "version": "11.2.4", "changelog": "blah 2", @@ -112,26 +123,54 @@ FIRMWARE_UPDATES = { } +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.UPDATE] + + +@pytest.fixture(name="controller_state", autouse=True) +def controller_state_fixture( + controller_state: dict[str, Any], +) -> dict[str, Any]: + """Load the controller state fixture data.""" + controller_state = deepcopy(controller_state) + # Set the minimum SDK version that supports firmware updates for controllers. + controller_state["controller"]["sdkVersion"] = "6.50.0" + return controller_state + + +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_states( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, caplog: pytest.LogCaptureFixture, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, ) -> None: """Test update entity states.""" ws_client = await hass_ws_client(hass) - assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF + assert client.driver.controller.sdk_version == "6.50.0" + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF client.async_send_command.return_value = {"updates": []} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -139,7 +178,7 @@ async def test_update_entity_states( { "id": 1, "type": "update/release_notes", - "entity_id": UPDATE_ENTITY, + "entity_id": entity_id, } ) result = await ws_client.receive_json() @@ -150,12 +189,12 @@ async def test_update_entity_states( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON attrs = state.attributes assert not attrs[ATTR_AUTO_UPDATE] - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert attrs[ATTR_RELEASE_URL] is None @@ -165,7 +204,7 @@ async def test_update_entity_states( { "id": 2, "type": "update/release_notes", - "entity_id": UPDATE_ENTITY, + "entity_id": entity_id, } ) result = await ws_client.receive_json() @@ -176,7 +215,7 @@ async def test_update_entity_states( DOMAIN, SERVICE_REFRESH_VALUE, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) @@ -188,31 +227,21 @@ async def test_update_entity_states( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=3)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF - # Assert a node firmware update entity is not created for the controller - driver = client.driver - node = driver.controller.nodes[1] - assert node.is_controller_node - assert ( - entity_registry.async_get_entity_id( - DOMAIN, - "sensor", - f"{get_valueless_base_unique_id(driver, node)}.firmware_update", - ) - is None - ) - - client.async_send_command.reset_mock() - +@pytest.mark.parametrize( + "entity_id", + [CONTROLLER_UPDATE_ENTITY, NODE_UPDATE_ENTITY], +) async def test_update_entity_install_raises( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, + entity_id: str, ) -> None: """Test update entity install raises exception.""" client.async_send_command.return_value = FIRMWARE_UPDATES @@ -228,7 +257,7 @@ async def test_update_entity_install_raises( UPDATE_DOMAIN, SERVICE_INSTALL, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) @@ -236,9 +265,9 @@ async def test_update_entity_install_raises( async def test_update_entity_sleep( hass: HomeAssistant, - client, - zen_31, - integration, + client: MagicMock, + zen_31: Node, + integration: MockConfigEntry, ) -> None: """Test update occurs when device is asleep after it wakes up.""" event = Event( @@ -253,8 +282,15 @@ async def test_update_entity_sleep( 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 + # Two nodes in total, the controller node and the zen_31 node. + # The zen_31 node is asleep, + # so we should only check for updates for the controller node. + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == 1 + + client.async_send_command.reset_mock() event = Event( "wake up", @@ -263,19 +299,20 @@ async def test_update_entity_sleep( zen_31.receive_event(event) await hass.async_block_till_done() - # Now that the node is up we can check for updates - assert len(client.async_send_command.call_args_list) > 0 - - args = client.async_send_command.call_args_list[0][0][0] + # Now that the zen_31 node is awake we can check for updates for it. + # The controller node has already been checked, + # so won't get another check now. + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == zen_31.node_id + assert args["nodeId"] == 94 async def test_update_entity_dead( hass: HomeAssistant, - client, - zen_31, - integration, + client: MagicMock, + zen_31: Node, + integration: MockConfigEntry, ) -> None: """Test update occurs even when device is dead.""" event = Event( @@ -290,18 +327,24 @@ 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() - # Checking for firmware updates should proceed even for dead nodes - assert len(client.async_send_command.call_args_list) > 0 + # Two nodes in total, the controller node and the zen_31 node. + # Checking for firmware updates should proceed even for dead nodes. + assert client.async_send_command.call_count == 2 + calls = sorted( + client.async_send_command.call_args_list, key=lambda call: call[0][0]["nodeId"] + ) - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == zen_31.node_id + node_ids = (1, 94) + for node_id, call in zip(node_ids, calls, strict=True): + args = call[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == node_id async def test_update_entity_ha_not_running( hass: HomeAssistant, - client, - zen_31, + client: MagicMock, + zen_31: Node, hass_ws_client: WebSocketGenerator, ) -> None: """Test update occurs only after HA is running.""" @@ -314,81 +357,170 @@ async def test_update_entity_ha_not_running( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 4 + client.async_send_command.reset_mock() + assert client.async_send_command.call_count == 0 await hass.async_start() await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 4 + assert client.async_send_command.call_count == 0 - # Update should be delayed by a day because HA is not running + # Update should be delayed by a day because Home Assistant is not running hass.set_state(CoreState.starting) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 4 + assert client.async_send_command.call_count == 0 hass.set_state(CoreState.running) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 5 - args = client.async_send_command.call_args_list[4][0][0] - assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == zen_31.node_id + # Two nodes in total, the controller node and the zen_31 node. + assert client.async_send_command.call_count == 2 + calls = sorted( + client.async_send_command.call_args_list, key=lambda call: call[0][0]["nodeId"] + ) + + node_ids = (1, 94) + for node_id, call in zip(node_ids, calls, strict=True): + args = call[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == node_id async def test_update_entity_update_failure( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, ) -> None: """Test update entity update failed.""" - assert len(client.async_send_command.call_args_list) == 0 + assert client.async_send_command.call_count == 0 client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) - assert state - assert state.state == STATE_OFF - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "controller.get_available_firmware_updates" - assert ( - args["nodeId"] - == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + entity_ids = (CONTROLLER_UPDATE_ENTITY, NODE_UPDATE_ENTITY) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + assert client.async_send_command.call_count == 2 + calls = sorted( + client.async_send_command.call_args_list, key=lambda call: call[0][0]["nodeId"] ) + node_ids = (1, 26) + for node_id, call in zip(node_ids, calls, strict=True): + args = call[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == node_id + +@pytest.mark.parametrize( + ( + "entity_id", + "installed_version", + "install_result", + "progress_event", + "finished_event", + ), + [ + ( + CONTROLLER_UPDATE_ENTITY, + "1.2", + {"status": 255, "success": True}, + Event( + type="firmware update progress", + data={ + "source": "driver", + "event": "firmware update progress", + "progress": { + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, + }, + ), + Event( + type="firmware update finished", + data={ + "source": "driver", + "event": "firmware update finished", + "result": { + "status": DriverFirmwareUpdateStatus.OK, + "success": True, + }, + }, + ), + ), + ( + NODE_UPDATE_ENTITY, + "10.7", + {"status": 254, "success": True, "reInterview": False}, + Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": 26, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, + }, + ), + Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": 26, + "result": { + "status": NodeFirmwareUpdateStatus.OK_NO_RESTART, + "success": True, + "reInterview": False, + }, + }, + ), + ), + ], +) async def test_update_entity_progress( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, + entity_id: str, + installed_version: str, + install_result: dict[str, Any], + progress_event: Event, + finished_event: Event, ) -> None: """Test update entity progress.""" - node = climate_radio_thermostat_ct100_plus_different_endpoints client.async_send_command.return_value = FIRMWARE_UPDATES + driver = client.driver async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON attrs = state.attributes - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = { - "result": {"status": 2, "success": False, "reInterview": False} - } + client.async_send_command.return_value = {"result": install_result} # Test successful install call without a version install_task = hass.async_create_task( @@ -396,64 +528,36 @@ async def test_update_entity_progress( UPDATE_DOMAIN, SERVICE_INSTALL, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) ) # Sleep so that task starts - await asyncio.sleep(0.1) + await asyncio.sleep(0.05) - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True assert attrs[ATTR_UPDATE_PERCENTAGE] is None - event = Event( - type="firmware update progress", - data={ - "source": "node", - "event": "firmware update progress", - "nodeId": node.node_id, - "progress": { - "currentFile": 1, - "totalFiles": 1, - "sentFragments": 1, - "totalFragments": 20, - "progress": 5.0, - }, - }, - ) - node.receive_event(event) + driver.receive_event(progress_event) + await asyncio.sleep(0.05) # Validate that the progress is updated - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True assert attrs[ATTR_UPDATE_PERCENTAGE] == 5 - event = Event( - type="firmware update finished", - data={ - "source": "node", - "event": "firmware update finished", - "nodeId": node.node_id, - "result": { - "status": NodeFirmwareUpdateStatus.OK_NO_RESTART, - "success": True, - "reInterview": False, - }, - }, - ) - - node.receive_event(event) + driver.receive_event(finished_event) await hass.async_block_till_done() # Validate that progress is reset and entity reflects new version - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is False @@ -465,31 +569,106 @@ async def test_update_entity_progress( await install_task +@pytest.mark.parametrize( + ( + "entity_id", + "installed_version", + "install_result", + "progress_event", + "finished_event", + ), + [ + ( + CONTROLLER_UPDATE_ENTITY, + "1.2", + {"status": 0, "success": False}, + Event( + type="firmware update progress", + data={ + "source": "driver", + "event": "firmware update progress", + "progress": { + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, + }, + ), + Event( + type="firmware update finished", + data={ + "source": "driver", + "event": "firmware update finished", + "result": { + "status": DriverFirmwareUpdateStatus.ERROR_TIMEOUT, + "success": False, + }, + }, + ), + ), + ( + NODE_UPDATE_ENTITY, + "10.7", + {"status": -1, "success": False, "reInterview": False}, + Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": 26, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, + }, + ), + Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": 26, + "result": { + "status": NodeFirmwareUpdateStatus.ERROR_TIMEOUT, + "success": False, + "reInterview": False, + }, + }, + ), + ), + ], +) async def test_update_entity_install_failed( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, caplog: pytest.LogCaptureFixture, + entity_id: str, + installed_version: str, + install_result: dict[str, Any], + progress_event: Event, + finished_event: Event, ) -> None: """Test update entity install returns error status.""" - node = climate_radio_thermostat_ct100_plus_different_endpoints + driver = client.driver client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON attrs = state.attributes - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = { - "result": {"status": 2, "success": False, "reInterview": False} - } + client.async_send_command.return_value = {"result": install_result} # Test install call - we expect it to finish fail install_task = hass.async_create_task( @@ -497,63 +676,35 @@ async def test_update_entity_install_failed( UPDATE_DOMAIN, SERVICE_INSTALL, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) ) # Sleep so that task starts - await asyncio.sleep(0.1) + await asyncio.sleep(0.05) - event = Event( - type="firmware update progress", - data={ - "source": "node", - "event": "firmware update progress", - "nodeId": node.node_id, - "progress": { - "currentFile": 1, - "totalFiles": 1, - "sentFragments": 1, - "totalFragments": 20, - "progress": 5.0, - }, - }, - ) - node.receive_event(event) + driver.receive_event(progress_event) + await asyncio.sleep(0.05) # Validate that the progress is updated - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True assert attrs[ATTR_UPDATE_PERCENTAGE] == 5 - event = Event( - type="firmware update finished", - data={ - "source": "node", - "event": "firmware update finished", - "nodeId": node.node_id, - "result": { - "status": NodeFirmwareUpdateStatus.ERROR_TIMEOUT, - "success": False, - "reInterview": False, - }, - }, - ) - - node.receive_event(event) + driver.receive_event(finished_event) await hass.async_block_till_done() # Validate that progress is reset and entity reflects old version - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_UPDATE_PERCENTAGE] is None - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_ON @@ -562,21 +713,30 @@ async def test_update_entity_install_failed( await install_task +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_reload( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, + entity_id: str, + installed_version: str, ) -> None: """Test update entity maintains state after reload.""" - assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF + config_entry = integration + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF client.async_send_command.return_value = {"updates": []} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -585,12 +745,12 @@ async def test_update_entity_reload( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON attrs = state.attributes assert not attrs[ATTR_AUTO_UPDATE] - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_UPDATE_PERCENTAGE] is None assert attrs[ATTR_LATEST_VERSION] == "11.2.4" @@ -600,24 +760,24 @@ async def test_update_entity_reload( UPDATE_DOMAIN, SERVICE_SKIP, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" - await hass.config_entries.async_reload(integration.entry_id) + await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() # Trigger another update and make sure the skipped version is still skipped async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=4)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" @@ -625,9 +785,9 @@ async def test_update_entity_reload( async def test_update_entity_delay( hass: HomeAssistant, - client, - ge_in_wall_dimmer_switch, - zen_31, + client: MagicMock, + ge_in_wall_dimmer_switch: Node, + zen_31: Node, hass_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, ) -> None: @@ -641,12 +801,13 @@ async def test_update_entity_delay( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 6 + client.async_send_command.reset_mock() + assert client.async_send_command.call_count == 0 await hass.async_start() await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 6 + assert client.async_send_command.call_count == 0 update_interval = timedelta(minutes=5) freezer.tick(update_interval) @@ -655,8 +816,8 @@ async def test_update_entity_delay( nodes: set[int] = set() - assert len(client.async_send_command.call_args_list) == 7 - args = client.async_send_command.call_args_list[6][0][0] + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "controller.get_available_firmware_updates" nodes.add(args["nodeId"]) @@ -664,30 +825,45 @@ async def test_update_entity_delay( async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 8 - args = client.async_send_command.call_args_list[7][0][0] + assert client.async_send_command.call_count == 2 + args = client.async_send_command.call_args[0][0] assert args["command"] == "controller.get_available_firmware_updates" nodes.add(args["nodeId"]) - assert len(nodes) == 2 - assert nodes == {ge_in_wall_dimmer_switch.node_id, zen_31.node_id} + freezer.tick(update_interval) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert client.async_send_command.call_count == 3 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + nodes.add(args["nodeId"]) + + assert len(nodes) == 3 + assert nodes == {1, ge_in_wall_dimmer_switch.node_id, zen_31.node_id} +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_partial_restore_data( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, ) -> None: """Test update entity with partial restore data resets state.""" mock_restore_cache( hass, [ State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: "11.2.4", ATTR_SKIPPED_VERSION: "11.2.4", }, @@ -699,16 +875,22 @@ async def test_update_entity_partial_restore_data( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_partial_restore_data_2( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, ) -> None: """Test second scenario where update entity has partial restore data.""" mock_restore_cache_with_extra_data( @@ -716,10 +898,10 @@ async def test_update_entity_partial_restore_data_2( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_ON, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: "10.8", ATTR_SKIPPED_VERSION: None, }, @@ -733,18 +915,24 @@ async def test_update_entity_partial_restore_data_2( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN assert state.attributes[ATTR_SKIPPED_VERSION] is None assert state.attributes[ATTR_LATEST_VERSION] is None +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_full_restore_data_skipped_version( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, ) -> None: """Test update entity with full restore data (skipped version) restores state.""" mock_restore_cache_with_extra_data( @@ -752,10 +940,10 @@ async def test_update_entity_full_restore_data_skipped_version( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: "11.2.4", ATTR_SKIPPED_VERSION: "11.2.4", }, @@ -769,18 +957,44 @@ async def test_update_entity_full_restore_data_skipped_version( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" assert state.attributes[ATTR_LATEST_VERSION] == "11.2.4" +@pytest.mark.parametrize( + ("entity_id", "installed_version", "install_result", "install_command_params"), + [ + ( + CONTROLLER_UPDATE_ENTITY, + "1.2", + {"status": 255, "success": True}, + { + "command": "driver.firmware_update_otw", + }, + ), + ( + NODE_UPDATE_ENTITY, + "10.7", + {"status": 255, "success": True, "reInterview": False}, + { + "command": "controller.firmware_update_ota", + "nodeId": 26, + }, + ), + ], +) async def test_update_entity_full_restore_data_update_available( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, + install_result: dict[str, Any], + install_command_params: dict[str, Any], ) -> None: """Test update entity with full restore data (update available) restores state.""" mock_restore_cache_with_extra_data( @@ -788,10 +1002,10 @@ async def test_update_entity_full_restore_data_update_available( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: "11.2.4", ATTR_SKIPPED_VERSION: None, }, @@ -805,15 +1019,14 @@ async def test_update_entity_full_restore_data_update_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON assert state.attributes[ATTR_SKIPPED_VERSION] is None assert state.attributes[ATTR_LATEST_VERSION] == "11.2.4" - client.async_send_command.return_value = { - "result": {"status": 255, "success": True, "reInterview": False} - } + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"result": install_result} # Test successful install call without a version install_task = hass.async_create_task( @@ -821,25 +1034,24 @@ async def test_update_entity_full_restore_data_update_available( UPDATE_DOMAIN, SERVICE_INSTALL, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) ) # Sleep so that task starts - await asyncio.sleep(0.1) + await asyncio.sleep(0.05) - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True assert attrs[ATTR_UPDATE_PERCENTAGE] is None - assert len(client.async_send_command.call_args_list) == 5 - assert client.async_send_command.call_args_list[4][0][0] == { - "command": "controller.firmware_update_ota", - "nodeId": climate_radio_thermostat_ct100_plus_different_endpoints.node_id, + assert client.async_send_command.call_count == 1 + assert client.async_send_command.call_args[0][0] == { + **install_command_params, "updateInfo": { "version": "11.2.4", "changelog": "blah 2", @@ -862,11 +1074,18 @@ async def test_update_entity_full_restore_data_update_available( install_task.cancel() +@pytest.mark.parametrize( + ("entity_id", "installed_version", "latest_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2", "1.2"), (NODE_UPDATE_ENTITY, "10.7", "10.7")], +) async def test_update_entity_full_restore_data_no_update_available( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, + latest_version: str, ) -> None: """Test entity with full restore data (no update available) restores state.""" mock_restore_cache_with_extra_data( @@ -874,11 +1093,11 @@ async def test_update_entity_full_restore_data_no_update_available( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", - ATTR_LATEST_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, + ATTR_LATEST_VERSION: latest_version, ATTR_SKIPPED_VERSION: None, }, ), @@ -891,18 +1110,25 @@ async def test_update_entity_full_restore_data_no_update_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] is None - assert state.attributes[ATTR_LATEST_VERSION] == "10.7" + assert state.attributes[ATTR_LATEST_VERSION] == latest_version +@pytest.mark.parametrize( + ("entity_id", "installed_version", "latest_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2", "1.2"), (NODE_UPDATE_ENTITY, "10.7", "10.7")], +) async def test_update_entity_no_latest_version( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, + latest_version: str, ) -> None: """Test entity with no `latest_version` attr restores state.""" mock_restore_cache_with_extra_data( @@ -910,10 +1136,10 @@ async def test_update_entity_no_latest_version( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: None, ATTR_SKIPPED_VERSION: None, }, @@ -927,24 +1153,33 @@ async def test_update_entity_no_latest_version( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] is None - assert state.attributes[ATTR_LATEST_VERSION] == "10.7" + assert state.attributes[ATTR_LATEST_VERSION] == latest_version async def test_update_entity_unload_asleep_node( - hass: HomeAssistant, client, wallmote_central_scene, integration + hass: HomeAssistant, + client: MagicMock, + wallmote_central_scene: Node, + integration: MockConfigEntry, ) -> None: """Test unloading config entry after attempting an update for an asleep node.""" - assert len(client.async_send_command.call_args_list) == 0 + config_entry = integration + assert client.async_send_command.call_count == 0 + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"updates": []} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 0 - assert len(wallmote_central_scene._listeners["wake up"]) == 2 + # Once call completed for the (awake) controller node. + assert client.async_send_command.call_count == 1 + assert len(wallmote_central_scene._listeners["wake up"]) == 1 - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) + assert client.async_send_command.call_count == 1 assert len(wallmote_central_scene._listeners["wake up"]) == 0 From 9d66b19c0328de6047d4feefa0d3700c8eb5bd2c Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 05:20:04 -0400 Subject: [PATCH 107/265] Add assumed optimistic to template number entities (#148499) --- homeassistant/components/template/number.py | 234 ++++++++++---------- tests/components/template/test_number.py | 103 +++++++-- 2 files changed, 205 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 31a6338f594..362a7e9d5c5 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -17,14 +17,9 @@ from homeassistant.components.number import ( NumberEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_NAME, - CONF_OPTIMISTIC, - CONF_STATE, - CONF_UNIT_OF_MEASUREMENT, -) +from homeassistant.const import CONF_NAME, CONF_STATE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, AddEntitiesCallback, @@ -33,6 +28,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import TriggerUpdateCoordinator from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN +from .entity import AbstractTemplateEntity from .helpers import ( async_setup_template_entry, async_setup_template_platform, @@ -40,6 +36,7 @@ from .helpers import ( ) from .template_entity import ( TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, + TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, ) @@ -57,21 +54,15 @@ NUMBER_COMMON_SCHEMA = vol.Schema( vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template, vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template, vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, - vol.Required(CONF_STATE): cv.template, - vol.Required(CONF_STEP): cv.template, + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.template, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } -) +).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) -NUMBER_YAML_SCHEMA = ( - vol.Schema( - { - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - } - ) - .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) - .extend(NUMBER_COMMON_SCHEMA.schema) -) +NUMBER_YAML_SCHEMA = NUMBER_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA +).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) NUMBER_CONFIG_ENTRY_SCHEMA = NUMBER_COMMON_SCHEMA.extend( TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema @@ -121,69 +112,28 @@ def async_create_preview_number( ) -class StateNumberEntity(TemplateEntity, NumberEntity): - """Representation of a template number.""" +class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity): + """Representation of a template number features.""" - _attr_should_poll = False _entity_id_format = ENTITY_ID_FORMAT + _optimistic_entity = True - def __init__( - self, - hass: HomeAssistant, - config, - unique_id: str | None, - ) -> None: - """Initialize the number.""" - TemplateEntity.__init__(self, hass, config, unique_id) - if TYPE_CHECKING: - assert self._attr_name is not None - - self._value_template = config[CONF_STATE] - self.add_script(CONF_SET_VALUE, config[CONF_SET_VALUE], self._attr_name, DOMAIN) - + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" self._step_template = config[CONF_STEP] - self._min_value_template = config[CONF_MIN] - self._max_value_template = config[CONF_MAX] - self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC) + self._min_template = config[CONF_MIN] + self._max_template = config[CONF_MAX] + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) self._attr_native_step = DEFAULT_STEP self._attr_native_min_value = DEFAULT_MIN_VALUE self._attr_native_max_value = DEFAULT_MAX_VALUE - @callback - def _async_setup_templates(self) -> None: - """Set up templates.""" - self.add_template_attribute( - "_attr_native_value", - self._value_template, - validator=vol.Coerce(float), - none_on_template_error=True, - ) - self.add_template_attribute( - "_attr_native_step", - self._step_template, - validator=vol.Coerce(float), - none_on_template_error=True, - ) - if self._min_value_template is not None: - self.add_template_attribute( - "_attr_native_min_value", - self._min_value_template, - validator=vol.Coerce(float), - none_on_template_error=True, - ) - if self._max_value_template is not None: - self.add_template_attribute( - "_attr_native_max_value", - self._max_value_template, - validator=vol.Coerce(float), - none_on_template_error=True, - ) - super()._async_setup_templates() - async def async_set_native_value(self, value: float) -> None: """Set value of the number.""" - if self._optimistic: + if self._attr_assumed_state: self._attr_native_value = value self.async_write_ha_state() if set_value := self._action_scripts.get(CONF_SET_VALUE): @@ -194,17 +144,65 @@ class StateNumberEntity(TemplateEntity, NumberEntity): ) -class TriggerNumberEntity(TriggerEntity, NumberEntity): +class StateNumberEntity(TemplateEntity, AbstractTemplateNumber): + """Representation of a template number.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + unique_id: str | None, + ) -> None: + """Initialize the number.""" + TemplateEntity.__init__(self, hass, config, unique_id) + AbstractTemplateNumber.__init__(self, config) + + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + self.add_script(CONF_SET_VALUE, config[CONF_SET_VALUE], name, DOMAIN) + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + if self._template is not None: + self.add_template_attribute( + "_attr_native_value", + self._template, + vol.Coerce(float), + none_on_template_error=True, + ) + if self._step_template is not None: + self.add_template_attribute( + "_attr_native_step", + self._step_template, + vol.Coerce(float), + none_on_template_error=True, + ) + if self._min_template is not None: + self.add_template_attribute( + "_attr_native_min_value", + self._min_template, + validator=vol.Coerce(float), + none_on_template_error=True, + ) + if self._max_template is not None: + self.add_template_attribute( + "_attr_native_max_value", + self._max_template, + validator=vol.Coerce(float), + none_on_template_error=True, + ) + super()._async_setup_templates() + + +class TriggerNumberEntity(TriggerEntity, AbstractTemplateNumber): """Number entity based on trigger data.""" - _entity_id_format = ENTITY_ID_FORMAT domain = NUMBER_DOMAIN - extra_template_keys = ( - CONF_STATE, - CONF_STEP, - CONF_MIN, - CONF_MAX, - ) def __init__( self, @@ -213,47 +211,49 @@ class TriggerNumberEntity(TriggerEntity, NumberEntity): config: dict, ) -> None: """Initialize the entity.""" - super().__init__(hass, coordinator, config) + TriggerEntity.__init__(self, hass, coordinator, config) + AbstractTemplateNumber.__init__(self, config) - name = self._rendered.get(CONF_NAME, DEFAULT_NAME) - self.add_script(CONF_SET_VALUE, config[CONF_SET_VALUE], name, DOMAIN) + for key in ( + CONF_STATE, + CONF_STEP, + CONF_MIN, + CONF_MAX, + ): + if isinstance(config.get(key), template.Template): + self._to_render_simple.append(key) + self._parse_result.add(key) - self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) - - @property - def native_value(self) -> float | None: - """Return the currently selected option.""" - return vol.Any(vol.Coerce(float), None)(self._rendered.get(CONF_STATE)) - - @property - def native_min_value(self) -> int: - """Return the minimum value.""" - return vol.Any(vol.Coerce(float), None)( - self._rendered.get(CONF_MIN, super().native_min_value) + self.add_script( + CONF_SET_VALUE, + config[CONF_SET_VALUE], + self._rendered.get(CONF_NAME, DEFAULT_NAME), + DOMAIN, ) - @property - def native_max_value(self) -> int: - """Return the maximum value.""" - return vol.Any(vol.Coerce(float), None)( - self._rendered.get(CONF_MAX, super().native_max_value) - ) + def _handle_coordinator_update(self): + """Handle updated data from the coordinator.""" + self._process_data() - @property - def native_step(self) -> int: - """Return the increment/decrement step.""" - return vol.Any(vol.Coerce(float), None)( - self._rendered.get(CONF_STEP, super().native_step) - ) - - async def async_set_native_value(self, value: float) -> None: - """Set value of the number.""" - if self._config[CONF_OPTIMISTIC]: - self._attr_native_value = value + if not self.available: + self.async_write_ha_state() + return + + write_ha_state = False + for key, attr in ( + (CONF_STATE, "_attr_native_value"), + (CONF_STEP, "_attr_native_step"), + (CONF_MIN, "_attr_native_min_value"), + (CONF_MAX, "_attr_native_max_value"), + ): + if (rendered := self._rendered.get(key)) is not None: + setattr(self, attr, vol.Any(vol.Coerce(float), None)(rendered)) + write_ha_state = True + + if len(self._rendered) > 0: + # In case any non optimistic template + write_ha_state = True + + if write_ha_state: + self.async_set_context(self.coordinator.data["context"]) self.async_write_ha_state() - if set_value := self._action_scripts.get(CONF_SET_VALUE): - await self.async_run_script( - set_value, - run_variables={ATTR_VALUE: value}, - context=self._context, - ) diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index 21dea28b73f..0ae98a23ae4 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_ICON, CONF_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import Context, HomeAssistant, ServiceCall @@ -63,11 +64,11 @@ _VALUE_INPUT_NUMBER_CONFIG = { } TEST_STATE_ENTITY_ID = "number.test_state" - +TEST_AVAILABILITY_ENTITY_ID = "binary_sensor.test_availability" TEST_STATE_TRIGGER = { "trigger": { "trigger": "state", - "entity_id": [TEST_STATE_ENTITY_ID], + "entity_id": [TEST_STATE_ENTITY_ID, TEST_AVAILABILITY_ENTITY_ID], }, "variables": {"triggering_entity": "{{ trigger.entity_id }}"}, "action": [ @@ -191,19 +192,6 @@ async def test_missing_optional_config(hass: HomeAssistant) -> None: async def test_missing_required_keys(hass: HomeAssistant) -> None: """Test: missing required fields will fail.""" - with assert_setup_component(0, "template"): - assert await setup.async_setup_component( - hass, - "template", - { - "template": { - "number": { - "set_value": {"service": "script.set_value"}, - } - } - }, - ) - with assert_setup_component(0, "template"): assert await setup.async_setup_component( hass, @@ -578,6 +566,91 @@ async def test_device_id( assert template_entity.device_id == device_entry.id +@pytest.mark.parametrize( + ("count", "number_config"), + [ + ( + 1, + { + "set_value": [], + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_number") +async def test_optimistic(hass: HomeAssistant) -> None: + """Test configuration with optimistic state.""" + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: _TEST_NUMBER, "value": 4}, + blocking=True, + ) + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 4 + + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: _TEST_NUMBER, "value": 2}, + blocking=True, + ) + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 2 + + +@pytest.mark.parametrize( + ("count", "number_config"), + [ + ( + 1, + { + "set_value": [], + "state": "{{ states('number.test_state') }}", + "availability": "{{ is_state('binary_sensor.test_availability', 'on') }}", + }, + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_number") +async def test_availability(hass: HomeAssistant) -> None: + """Test configuration with optimistic state.""" + + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, "on") + hass.states.async_set(TEST_STATE_ENTITY_ID, "4.0") + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 4 + + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, "off") + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set(TEST_STATE_ENTITY_ID, "2.0") + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, "on") + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 2 + + @pytest.mark.parametrize( ("count", "number_config"), [ From 06233b5134c6586810833d4eedaa4b85ca7ab5bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Jul 2025 00:16:16 -1000 Subject: [PATCH 108/265] Bump aioesphomeapi to 37.1.5 (#149656) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 17fd72fc939..00d56955aa7 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.1.2", + "aioesphomeapi==37.1.5", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index eafa0b0d47f..fd13a55446b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.2 +aioesphomeapi==37.1.5 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4ed33e539b..cb329428196 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.2 +aioesphomeapi==37.1.5 # homeassistant.components.flo aioflo==2021.11.0 From 03ee97d38f28761ce4a6ff29e5c64f2418d00208 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 30 Jul 2025 12:16:40 +0200 Subject: [PATCH 109/265] Clarify description of `turn_away_mode_on.osoenergy` action (#149655) --- homeassistant/components/osoenergy/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/osoenergy/strings.json b/homeassistant/components/osoenergy/strings.json index 60b67731eac..48b99749ca1 100644 --- a/homeassistant/components/osoenergy/strings.json +++ b/homeassistant/components/osoenergy/strings.json @@ -211,7 +211,7 @@ }, "turn_away_mode_on": { "name": "Set away mode", - "description": "Turns away mode on for the heater", + "description": "Turns on away mode for the water heater", "fields": { "duration_days": { "name": "Duration in days", From ac86f2e2ba9247429dd332a66f09eadc9f0b5449 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 30 Jul 2025 12:21:27 +0200 Subject: [PATCH 110/265] Add Frient brand (#149654) --- homeassistant/brands/frient.json | 5 +++++ homeassistant/generated/integrations.json | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 homeassistant/brands/frient.json diff --git a/homeassistant/brands/frient.json b/homeassistant/brands/frient.json new file mode 100644 index 00000000000..e6b4374576f --- /dev/null +++ b/homeassistant/brands/frient.json @@ -0,0 +1,5 @@ +{ + "domain": "frient", + "name": "Frient", + "iot_standards": ["zigbee"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5f4ae434074..1eb37ae87d2 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2137,6 +2137,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "frient": { + "name": "Frient", + "iot_standards": [ + "zigbee" + ] + }, "fritzbox": { "name": "FRITZ!Box", "integrations": { From a79d2da9a3765703e68504804bb3b38525db7b9d Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 30 Jul 2025 03:31:32 -0700 Subject: [PATCH 111/265] Move group toggle descriptions to data_description (#149625) Co-authored-by: Joost Lekkerkerker Co-authored-by: Martin Hjelmare --- homeassistant/components/group/strings.json | 23 ++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index b80b78027bf..bb9ab4b25d8 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -21,12 +21,14 @@ }, "binary_sensor": { "title": "[%key:component::group::config::step::user::title%]", - "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", "data": { "all": "All entities", "entities": "Members", "hide_members": "Hide members", "name": "[%key:common::config_flow::data::name%]" + }, + "data_description": { + "all": "If enabled, the group's state is on only if all members are on. If disabled, the group's state is on if any member is on." } }, "button": { @@ -105,6 +107,9 @@ "device_class": "Device class", "state_class": "State class", "unit_of_measurement": "Unit of measurement" + }, + "data_description": { + "ignore_non_numeric": "If enabled, the group's state is calculated if at least one member has a numerical value. If disabled, the group's state is calculated only if all group members have numerical values." } }, "switch": { @@ -120,11 +125,13 @@ "options": { "step": { "binary_sensor": { - "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + }, + "data_description": { + "all": "[%key:component::group::config::step::binary_sensor::data_description::all%]" } }, "button": { @@ -146,11 +153,13 @@ } }, "light": { - "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + }, + "data_description": { + "all": "[%key:component::group::config::step::binary_sensor::data_description::all%]" } }, "lock": { @@ -172,7 +181,6 @@ } }, "sensor": { - "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", "data": { "ignore_non_numeric": "[%key:component::group::config::step::sensor::data::ignore_non_numeric%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", @@ -182,14 +190,19 @@ "device_class": "[%key:component::group::config::step::sensor::data::device_class%]", "state_class": "[%key:component::group::config::step::sensor::data::state_class%]", "unit_of_measurement": "[%key:component::group::config::step::sensor::data::unit_of_measurement%]" + }, + "data_description": { + "ignore_non_numeric": "[%key:component::group::config::step::sensor::data_description::ignore_non_numeric%]" } }, "switch": { - "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + }, + "data_description": { + "all": "[%key:component::group::config::step::binary_sensor::data_description::all%]" } } } From 15e45df8a7e868902a8e43022ea57e0878853f5e Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 30 Jul 2025 13:49:21 +0300 Subject: [PATCH 112/265] Use async_create_clientsession in Alexa Devices (#149432) --- homeassistant/components/alexa_devices/__init__.py | 10 ++++------ homeassistant/components/alexa_devices/config_flow.py | 10 ++++------ homeassistant/components/alexa_devices/coordinator.py | 3 +++ homeassistant/components/alexa_devices/manifest.json | 2 +- .../components/alexa_devices/quality_scale.yaml | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index fe623c10b33..d18e730afcb 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -2,6 +2,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator @@ -16,7 +17,8 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Set up Alexa Devices platform.""" - coordinator = AmazonDevicesCoordinator(hass, entry) + session = aiohttp_client.async_create_clientsession(hass) + coordinator = AmazonDevicesCoordinator(hass, entry, session) await coordinator.async_config_entry_first_refresh() @@ -29,8 +31,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Unload a config entry.""" - coordinator = entry.runtime_data - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - await coordinator.api.close() - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index 5ee3bc2e5f0..3e705d73ade 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -17,6 +17,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import CountrySelector @@ -33,18 +34,15 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema( async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" + session = aiohttp_client.async_create_clientsession(hass) api = AmazonEchoApi( + session, data[CONF_COUNTRY], data[CONF_USERNAME], data[CONF_PASSWORD], ) - try: - data = await api.login_mode_interactive(data[CONF_CODE]) - finally: - await api.close() - - return data + return await api.login_mode_interactive(data[CONF_CODE]) class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index 7af66f4bb8b..f4a1faa4f81 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -8,6 +8,7 @@ from aioamazondevices.exceptions import ( CannotConnect, CannotRetrieveData, ) +from aiohttp import ClientSession from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME @@ -31,6 +32,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): self, hass: HomeAssistant, entry: AmazonConfigEntry, + session: ClientSession, ) -> None: """Initialize the scanner.""" super().__init__( @@ -41,6 +43,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): update_interval=timedelta(seconds=SCAN_INTERVAL), ) self.api = AmazonEchoApi( + session, entry.data[CONF_COUNTRY], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 74187ba7ed4..90410412dfa 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "silver", - "requirements": ["aioamazondevices==3.5.1"] + "requirements": ["aioamazondevices==4.0.0"] } diff --git a/homeassistant/components/alexa_devices/quality_scale.yaml b/homeassistant/components/alexa_devices/quality_scale.yaml index 95433655212..5a2ff55b9b2 100644 --- a/homeassistant/components/alexa_devices/quality_scale.yaml +++ b/homeassistant/components/alexa_devices/quality_scale.yaml @@ -70,5 +70,5 @@ rules: # Platinum async-dependency: done - inject-websession: todo + inject-websession: done strict-typing: done diff --git a/requirements_all.txt b/requirements_all.txt index fd13a55446b..93663598733 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.5.1 +aioamazondevices==4.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb329428196..268d263220a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.5.1 +aioamazondevices==4.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 5930ac6425e06cb9097f7b58a927fb7e6a972186 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:27:24 +0200 Subject: [PATCH 113/265] Use translation_placeholders in tuya switch descriptions (#149664) --- homeassistant/components/tuya/strings.json | 80 ++--------- homeassistant/components/tuya/switch.py | 132 ++++++++++++------ .../tuya/snapshots/test_switch.ambr | 14 +- 3 files changed, 105 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index fd3a680ed3c..97d623d7c21 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -744,86 +744,26 @@ "switch": { "name": "Switch" }, + "indexed_switch": { + "name": "Switch {index}" + }, "socket": { "name": "Socket" }, + "indexed_socket": { + "name": "Socket {index}" + }, "radio": { "name": "Radio" }, - "alarm_1": { - "name": "Alarm 1" - }, - "alarm_2": { - "name": "Alarm 2" - }, - "alarm_3": { - "name": "Alarm 3" - }, - "alarm_4": { - "name": "Alarm 4" + "indexed_alarm": { + "name": "Alarm {index}" }, "sleep_aid": { "name": "Sleep aid" }, - "switch_1": { - "name": "Switch 1" - }, - "switch_2": { - "name": "Switch 2" - }, - "switch_3": { - "name": "Switch 3" - }, - "switch_4": { - "name": "Switch 4" - }, - "switch_5": { - "name": "Switch 5" - }, - "switch_6": { - "name": "Switch 6" - }, - "switch_7": { - "name": "Switch 7" - }, - "switch_8": { - "name": "Switch 8" - }, - "usb_1": { - "name": "USB 1" - }, - "usb_2": { - "name": "USB 2" - }, - "usb_3": { - "name": "USB 3" - }, - "usb_4": { - "name": "USB 4" - }, - "usb_5": { - "name": "USB 5" - }, - "usb_6": { - "name": "USB 6" - }, - "socket_1": { - "name": "Socket 1" - }, - "socket_2": { - "name": "Socket 2" - }, - "socket_3": { - "name": "Socket 3" - }, - "socket_4": { - "name": "Socket 4" - }, - "socket_5": { - "name": "Socket 5" - }, - "socket_6": { - "name": "Socket 6" + "indexed_usb": { + "name": "USB {index}" }, "ionizer": { "name": "Ionizer" diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 67f3ba9cb81..f6d5df9af73 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -232,35 +232,43 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "ggq": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="switch_1", + translation_key="indexed_switch", + translation_placeholders={"index": "1"}, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="switch_2", + translation_key="indexed_switch", + translation_placeholders={"index": "2"}, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="switch_3", + translation_key="indexed_switch", + translation_placeholders={"index": "3"}, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="switch_4", + translation_key="indexed_switch", + translation_placeholders={"index": "4"}, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - translation_key="switch_5", + translation_key="indexed_switch", + translation_placeholders={"index": "5"}, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - translation_key="switch_6", + translation_key="indexed_switch", + translation_placeholders={"index": "6"}, ), SwitchEntityDescription( key=DPCode.SWITCH_7, - translation_key="switch_7", + translation_key="indexed_switch", + translation_placeholders={"index": "7"}, ), SwitchEntityDescription( key=DPCode.SWITCH_8, - translation_key="switch_8", + translation_key="indexed_switch", + translation_placeholders={"index": "8"}, ), ), # Wake Up Light II @@ -272,22 +280,26 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="alarm_1", + translation_key="indexed_alarm", + translation_placeholders={"index": "1"}, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="alarm_2", + translation_key="indexed_alarm", + translation_placeholders={"index": "2"}, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="alarm_3", + translation_key="indexed_alarm", + translation_placeholders={"index": "3"}, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - translation_key="alarm_4", + translation_key="indexed_alarm", + translation_placeholders={"index": "4"}, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( @@ -324,67 +336,81 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { ), SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="switch_1", + translation_key="indexed_switch", + translation_placeholders={"index": "1"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="switch_2", + translation_key="indexed_switch", + translation_placeholders={"index": "2"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="switch_3", + translation_key="indexed_switch", + translation_placeholders={"index": "3"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="switch_4", + translation_key="indexed_switch", + translation_placeholders={"index": "4"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - translation_key="switch_5", + translation_key="indexed_switch", + translation_placeholders={"index": "5"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - translation_key="switch_6", + translation_key="indexed_switch", + translation_placeholders={"index": "6"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_7, - translation_key="switch_7", + translation_key="indexed_switch", + translation_placeholders={"index": "7"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_8, - translation_key="switch_8", + translation_key="indexed_switch", + translation_placeholders={"index": "8"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, - translation_key="usb_1", + translation_key="indexed_usb", + translation_placeholders={"index": "1"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB2, - translation_key="usb_2", + translation_key="indexed_usb", + translation_placeholders={"index": "2"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB3, - translation_key="usb_3", + translation_key="indexed_usb", + translation_placeholders={"index": "3"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB4, - translation_key="usb_4", + translation_key="indexed_usb", + translation_placeholders={"index": "4"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB5, - translation_key="usb_5", + translation_key="indexed_usb", + translation_placeholders={"index": "5"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB6, - translation_key="usb_6", + translation_key="indexed_usb", + translation_placeholders={"index": "6"}, ), SwitchEntityDescription( key=DPCode.SWITCH, @@ -487,57 +513,69 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { ), SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="socket_1", + translation_key="indexed_socket", + translation_placeholders={"index": "1"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="socket_2", + translation_key="indexed_socket", + translation_placeholders={"index": "2"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="socket_3", + translation_key="indexed_socket", + translation_placeholders={"index": "3"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="socket_4", + translation_key="indexed_socket", + translation_placeholders={"index": "4"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - translation_key="socket_5", + translation_key="indexed_socket", + translation_placeholders={"index": "5"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - translation_key="socket_6", + translation_key="indexed_socket", + translation_placeholders={"index": "6"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, - translation_key="usb_1", + translation_key="indexed_usb", + translation_placeholders={"index": "1"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB2, - translation_key="usb_2", + translation_key="indexed_usb", + translation_placeholders={"index": "2"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB3, - translation_key="usb_3", + translation_key="indexed_usb", + translation_placeholders={"index": "3"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB4, - translation_key="usb_4", + translation_key="indexed_usb", + translation_placeholders={"index": "4"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB5, - translation_key="usb_5", + translation_key="indexed_usb", + translation_placeholders={"index": "5"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB6, - translation_key="usb_6", + translation_key="indexed_usb", + translation_placeholders={"index": "6"}, ), SwitchEntityDescription( key=DPCode.SWITCH, @@ -698,22 +736,26 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "tdq": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="switch_1", + translation_key="indexed_switch", + translation_placeholders={"index": "1"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="switch_2", + translation_key="indexed_switch", + translation_placeholders={"index": "2"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="switch_3", + translation_key="indexed_switch", + translation_placeholders={"index": "3"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="switch_4", + translation_key="indexed_switch", + translation_placeholders={"index": "4"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( @@ -746,12 +788,14 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "wkcz": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="switch_1", + translation_key="indexed_switch", + translation_placeholders={"index": "1"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="switch_2", + translation_key="indexed_switch", + translation_placeholders={"index": "2"}, device_class=SwitchDeviceClass.OUTLET, ), ), diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 92243414892..71aa05329aa 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -464,7 +464,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'socket_1', + 'translation_key': 'indexed_socket', 'unique_id': 'tuya.eb0c772dabbb19d653ssi5switch_1', 'unit_of_measurement': None, }) @@ -513,7 +513,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'socket_2', + 'translation_key': 'indexed_socket', 'unique_id': 'tuya.eb0c772dabbb19d653ssi5switch_2', 'unit_of_measurement': None, }) @@ -658,7 +658,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_1', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.0665305284f3ebe9fdc1switch_1', 'unit_of_measurement': None, }) @@ -995,7 +995,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_1', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_1', 'unit_of_measurement': None, }) @@ -1044,7 +1044,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_2', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_2', 'unit_of_measurement': None, }) @@ -1093,7 +1093,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_3', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_3', 'unit_of_measurement': None, }) @@ -1142,7 +1142,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_4', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_4', 'unit_of_measurement': None, }) From 1eb6d5fe3279519e0bc3f1062b27aa09d3c5a040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 30 Jul 2025 13:35:24 +0200 Subject: [PATCH 114/265] Add action for set_program_oven to miele (#149620) --- homeassistant/components/miele/icons.json | 3 + homeassistant/components/miele/services.py | 58 +++++++++++++++++- homeassistant/components/miele/services.yaml | 30 ++++++++++ homeassistant/components/miele/strings.json | 25 ++++++++ tests/components/miele/test_services.py | 62 +++++++++++++++++++- 5 files changed, 173 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/miele/icons.json b/homeassistant/components/miele/icons.json index 4a0eac7da85..77d94c49ffa 100644 --- a/homeassistant/components/miele/icons.json +++ b/homeassistant/components/miele/icons.json @@ -110,6 +110,9 @@ }, "set_program": { "service": "mdi:arrow-right-circle-outline" + }, + "set_program_oven": { + "service": "mdi:arrow-right-circle-outline" } } } diff --git a/homeassistant/components/miele/services.py b/homeassistant/components/miele/services.py index 3d73c021b3d..9854196ea65 100644 --- a/homeassistant/components/miele/services.py +++ b/homeassistant/components/miele/services.py @@ -1,12 +1,13 @@ """Services for Miele integration.""" +from datetime import timedelta import logging from typing import cast import aiohttp import voluptuous as vol -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE from homeassistant.core import ( HomeAssistant, ServiceCall, @@ -32,6 +33,19 @@ SERVICE_SET_PROGRAM_SCHEMA = vol.Schema( }, ) +SERVICE_SET_PROGRAM_OVEN = "set_program_oven" +SERVICE_SET_PROGRAM_OVEN_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_PROGRAM_ID): cv.positive_int, + vol.Optional(ATTR_TEMPERATURE): cv.positive_int, + vol.Optional(ATTR_DURATION): vol.All( + cv.time_period, + vol.Range(min=timedelta(minutes=1), max=timedelta(hours=12)), + ), + }, +) + SERVICE_GET_PROGRAMS = "get_programs" SERVICE_GET_PROGRAMS_SCHEMA = vol.Schema( { @@ -103,6 +117,36 @@ async def set_program(call: ServiceCall) -> None: ) from ex +async def set_program_oven(call: ServiceCall) -> None: + """Set a program on a Miele oven.""" + + _LOGGER.debug("Set program call: %s", call) + config_entry = await _extract_config_entry(call) + api = config_entry.runtime_data.api + + serial_number = await _get_serial_number(call) + data = {"programId": call.data[ATTR_PROGRAM_ID]} + if call.data.get(ATTR_DURATION) is not None: + td = call.data[ATTR_DURATION] + data["duration"] = [ + td.seconds // 3600, # hours + (td.seconds // 60) % 60, # minutes + ] + if call.data.get(ATTR_TEMPERATURE) is not None: + data["temperature"] = call.data[ATTR_TEMPERATURE] + try: + await api.set_program(serial_number, data) + except aiohttp.ClientResponseError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_program_oven_error", + translation_placeholders={ + "status": str(ex.status), + "message": ex.message, + }, + ) from ex + + async def get_programs(call: ServiceCall) -> ServiceResponse: """Get available programs from appliance.""" @@ -172,7 +216,17 @@ async def async_setup_services(hass: HomeAssistant) -> None: """Set up services.""" hass.services.async_register( - DOMAIN, SERVICE_SET_PROGRAM, set_program, SERVICE_SET_PROGRAM_SCHEMA + DOMAIN, + SERVICE_SET_PROGRAM, + set_program, + SERVICE_SET_PROGRAM_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_PROGRAM_OVEN, + set_program_oven, + SERVICE_SET_PROGRAM_OVEN_SCHEMA, ) hass.services.async_register( diff --git a/homeassistant/components/miele/services.yaml b/homeassistant/components/miele/services.yaml index 6866e997c45..87114343ad1 100644 --- a/homeassistant/components/miele/services.yaml +++ b/homeassistant/components/miele/services.yaml @@ -23,3 +23,33 @@ set_program: max: 99999 mode: box example: 24 + +set_program_oven: + fields: + device_id: + selector: + device: + integration: miele + required: true + program_id: + required: true + selector: + number: + min: 0 + max: 99999 + mode: box + example: 24 + temperature: + required: false + selector: + number: + min: 30 + max: 300 + unit_of_measurement: "°C" + mode: box + example: 180 + duration: + required: false + selector: + duration: + example: 1:15:00 diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 5b5cac16b53..cec4a63feec 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -1068,6 +1068,9 @@ "set_program_error": { "message": "'Set program' action failed {status} / {message}." }, + "set_program_oven_error": { + "message": "'Set program on oven' action failed {status} / {message}." + }, "set_state_error": { "message": "Failed to set state for {entity}." } @@ -1096,6 +1099,28 @@ "name": "Program ID" } } + }, + "set_program_oven": { + "name": "Set program on oven", + "description": "[%key:component::miele::services::set_program::description%]", + "fields": { + "device_id": { + "description": "[%key:component::miele::services::set_program::fields::device_id::description%]", + "name": "[%key:component::miele::services::set_program::fields::device_id::name%]" + }, + "program_id": { + "description": "[%key:component::miele::services::set_program::fields::program_id::description%]", + "name": "[%key:component::miele::services::set_program::fields::program_id::name%]" + }, + "temperature": { + "description": "The target temperature for the oven program.", + "name": "[%key:component::sensor::entity_component::temperature::name%]" + }, + "duration": { + "description": "The duration for the oven program.", + "name": "[%key:component::sensor::entity_component::duration::name%]" + } + } } } } diff --git a/tests/components/miele/test_services.py b/tests/components/miele/test_services.py index 2bf0e2deb9c..38b9f064b55 100644 --- a/tests/components/miele/test_services.py +++ b/tests/components/miele/test_services.py @@ -1,5 +1,6 @@ """Tests the services provided by the miele integration.""" +from datetime import timedelta from unittest.mock import MagicMock from aiohttp import ClientResponseError @@ -9,11 +10,13 @@ from voluptuous import MultipleInvalid from homeassistant.components.miele.const import DOMAIN from homeassistant.components.miele.services import ( + ATTR_DURATION, ATTR_PROGRAM_ID, SERVICE_GET_PROGRAMS, SERVICE_SET_PROGRAM, + SERVICE_SET_PROGRAM_OVEN, ) -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceRegistry @@ -49,6 +52,50 @@ async def test_services( ) +@pytest.mark.parametrize( + ("call_arguments", "miele_arguments"), + [ + ( + {ATTR_PROGRAM_ID: 24}, + {"programId": 24}, + ), + ( + {ATTR_PROGRAM_ID: 25, ATTR_DURATION: timedelta(minutes=75)}, + {"programId": 25, "duration": [1, 15]}, + ), + ( + { + ATTR_PROGRAM_ID: 26, + ATTR_DURATION: timedelta(minutes=135), + ATTR_TEMPERATURE: 180, + }, + {"programId": 26, "duration": [2, 15], "temperature": 180}, + ), + ], +) +async def test_services_oven( + hass: HomeAssistant, + device_registry: DeviceRegistry, + mock_miele_client: MagicMock, + mock_config_entry: MockConfigEntry, + call_arguments: dict, + miele_arguments: dict, +) -> None: + """Tests that the custom services are correct for ovens.""" + + await setup_integration(hass, mock_config_entry) + device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_APPLIANCE)}) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PROGRAM_OVEN, + {ATTR_DEVICE_ID: device.id, **call_arguments}, + blocking=True, + ) + mock_miele_client.set_program.assert_called_once_with( + TEST_APPLIANCE, miele_arguments + ) + + async def test_services_with_response( hass: HomeAssistant, device_registry: DeviceRegistry, @@ -71,11 +118,20 @@ async def test_services_with_response( ) +@pytest.mark.parametrize( + ("service", "error"), + [ + (SERVICE_SET_PROGRAM, "'Set program' action failed"), + (SERVICE_SET_PROGRAM_OVEN, "'Set program on oven' action failed"), + ], +) async def test_service_api_errors( hass: HomeAssistant, device_registry: DeviceRegistry, mock_miele_client: MagicMock, mock_config_entry: MockConfigEntry, + service: str, + error: str, ) -> None: """Test service api errors.""" await setup_integration(hass, mock_config_entry) @@ -83,10 +139,10 @@ async def test_service_api_errors( # Test http error mock_miele_client.set_program.side_effect = ClientResponseError("TestInfo", "test") - with pytest.raises(HomeAssistantError, match="'Set program' action failed"): + with pytest.raises(HomeAssistantError, match=error): await hass.services.async_call( DOMAIN, - SERVICE_SET_PROGRAM, + service, {ATTR_DEVICE_ID: device.id, ATTR_PROGRAM_ID: 1}, blocking=True, ) From 828f979c782c1c610b7b424221867b871e20ee72 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:43:07 +0200 Subject: [PATCH 115/265] Use Tuya device listener in binary sensor tests (#148890) --- tests/components/tuya/__init__.py | 26 ++++++++++++++++++++- tests/components/tuya/conftest.py | 12 ++++++++++ tests/components/tuya/test_binary_sensor.py | 13 ++++++++--- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index ab2d28ef645..039b8f29290 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations +from typing import Any from unittest.mock import patch from tuya_sharing import CustomerDevice -from homeassistant.components.tuya import ManagerCompat +from homeassistant.components.tuya import DeviceListener, ManagerCompat from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -180,6 +181,29 @@ DEVICE_MOCKS = { } +class MockDeviceListener(DeviceListener): + """Mocked DeviceListener for testing.""" + + async def async_send_device_update( + self, + hass: HomeAssistant, + device: CustomerDevice, + updated_status_properties: dict[str, Any] | None = None, + ) -> None: + """Mock update device method.""" + property_list: list[str] = [] + if updated_status_properties: + for key, value in updated_status_properties.items(): + if key not in device.status: + raise ValueError( + f"Property {key} not found in device status: {device.status}" + ) + device.status[key] = value + property_list.append(key) + self.update_device(device, property_list) + await hass.async_block_till_done() + + async def initialize_entry( hass: HomeAssistant, mock_manager: ManagerCompat, diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index cac9359a8d3..73752590637 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -21,6 +21,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.json import json_dumps from homeassistant.util import dt as dt_util +from . import MockDeviceListener + from tests.common import MockConfigEntry, async_load_json_object_fixture @@ -184,3 +186,13 @@ async def mock_device(hass: HomeAssistant, mock_device_code: str) -> CustomerDev if device.status_range[key].type == "Json": device.status[key] = json_dumps(value) return device + + +@pytest.fixture +def mock_listener( + hass: HomeAssistant, mock_manager: ManagerCompat +) -> MockDeviceListener: + """Create a DeviceListener for testing.""" + listener = MockDeviceListener(hass, mock_manager) + mock_manager.add_device_listener(listener) + return listener diff --git a/tests/components/tuya/test_binary_sensor.py b/tests/components/tuya/test_binary_sensor.py index f59e325b6cc..9045b28bfa9 100644 --- a/tests/components/tuya/test_binary_sensor.py +++ b/tests/components/tuya/test_binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import DEVICE_MOCKS, MockDeviceListener, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -78,16 +78,23 @@ async def test_bitmap( mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, mock_device: CustomerDevice, + mock_listener: MockDeviceListener, fault_value: int, tankfull: str, defrost: str, wet: str, ) -> None: """Test BITMAP fault sensor on cs_arete_two_12l_dehumidifier_air_purifier.""" - mock_device.status["fault"] = fault_value - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + assert hass.states.get("binary_sensor.dehumidifier_tank_full").state == "off" + assert hass.states.get("binary_sensor.dehumidifier_defrost").state == "off" + assert hass.states.get("binary_sensor.dehumidifier_wet").state == "off" + + await mock_listener.async_send_device_update( + hass, mock_device, {"fault": fault_value} + ) + assert hass.states.get("binary_sensor.dehumidifier_tank_full").state == tankfull assert hass.states.get("binary_sensor.dehumidifier_defrost").state == defrost assert hass.states.get("binary_sensor.dehumidifier_wet").state == wet From 749fc318ca72471a6bc0ca07c12fb08616e0f586 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:22:55 +0200 Subject: [PATCH 116/265] Validate selectors in the trigger helper (#149662) --- homeassistant/helpers/trigger.py | 9 ++++-- tests/helpers/test_trigger.py | 49 ++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 46b3d883865..de3f71c4834 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_ENABLED, CONF_ID, CONF_PLATFORM, + CONF_SELECTOR, CONF_VARIABLES, ) from homeassistant.core import ( @@ -41,8 +42,9 @@ from homeassistant.util.hass_dict import HassKey from homeassistant.util.yaml import load_yaml_dict from homeassistant.util.yaml.loader import JSON_TYPE -from . import config_validation as cv +from . import config_validation as cv, selector from .integration_platform import async_process_integration_platforms +from .selector import TargetSelector from .template import Template from .typing import ConfigType, TemplateVarsType @@ -73,12 +75,15 @@ TRIGGERS: HassKey[dict[str, str]] = HassKey("triggers") # Basic schemas to sanity check the trigger descriptions, # full validation is done by hassfest.triggers _FIELD_SCHEMA = vol.Schema( - {}, + { + vol.Optional(CONF_SELECTOR): selector.validate_selector, + }, extra=vol.ALLOW_EXTRA, ) _TRIGGER_SCHEMA = vol.Schema( { + vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None), vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}), }, extra=vol.ALLOW_EXTRA, diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index ba9db9cb053..050420d0195 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -569,7 +569,15 @@ async def test_async_get_all_descriptions( ) -> None: """Test async_get_all_descriptions.""" tag_trigger_descriptions = """ - tag: {} + tag: + fields: + entity: + selector: + entity: + filter: + domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME """ assert await async_setup_component(hass, DOMAIN_SUN, {}) @@ -611,9 +619,16 @@ async def test_async_get_all_descriptions( "fields": { "event": { "example": "sunrise", - "selector": {"select": {"options": ["sunrise", "sunset"]}}, + "selector": { + "select": { + "custom_value": False, + "multiple": False, + "options": ["sunrise", "sunset"], + "sort": False, + } + }, }, - "offset": {"selector": {"time": None}}, + "offset": {"selector": {"time": {}}}, } } } @@ -639,13 +654,35 @@ async def test_async_get_all_descriptions( "fields": { "event": { "example": "sunrise", - "selector": {"select": {"options": ["sunrise", "sunset"]}}, + "selector": { + "select": { + "custom_value": False, + "multiple": False, + "options": ["sunrise", "sunset"], + "sort": False, + } + }, }, - "offset": {"selector": {"time": None}}, + "offset": {"selector": {"time": {}}}, } }, DOMAIN_TAG: { - "fields": {}, + "fields": { + "entity": { + "selector": { + "entity": { + "filter": [ + { + "domain": ["alarm_control_panel"], + "supported_features": [1], + } + ], + "multiple": False, + "reorder": False, + }, + }, + }, + } }, } From 6c2a6628387bf208e62b1079154f937fc0602bf4 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:48:24 -0400 Subject: [PATCH 117/265] Add config flow to template cover platform (#149433) --- .../components/template/config_flow.py | 42 ++++++++++ homeassistant/components/template/cover.py | 77 +++++++++++++++---- homeassistant/components/template/helpers.py | 4 +- .../components/template/strings.json | 76 ++++++++++++++++++ .../template/snapshots/test_cover.ambr | 16 ++++ tests/components/template/test_config_flow.py | 50 ++++++++++++ tests/components/template/test_cover.py | 55 ++++++++++++- 7 files changed, 303 insertions(+), 17 deletions(-) create mode 100644 tests/components/template/snapshots/test_cover.ambr diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 7e06ef51a4b..7963f525b7a 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass +from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASS_STATE_CLASSES, @@ -62,6 +63,15 @@ from .const import ( CONF_TURN_ON, DOMAIN, ) +from .cover import ( + CLOSE_ACTION, + CONF_OPEN_AND_CLOSE, + CONF_POSITION, + OPEN_ACTION, + POSITION_ACTION, + STOP_ACTION, + async_create_preview_cover, +) from .number import ( CONF_MAX, CONF_MIN, @@ -143,6 +153,26 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: ) } + if domain == Platform.COVER: + schema |= _SCHEMA_STATE | { + vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): selector.ActionSelector(), + vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): selector.ActionSelector(), + vol.Optional(STOP_ACTION): selector.ActionSelector(), + vol.Optional(CONF_POSITION): selector.TemplateSelector(), + vol.Optional(POSITION_ACTION): selector.ActionSelector(), + } + if flow_type == "config": + schema |= { + vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[cls.value for cls in CoverDeviceClass], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="cover_device_class", + sort=True, + ), + ) + } + if domain == Platform.IMAGE: schema |= { vol.Required(CONF_URL): selector.TemplateSelector(), @@ -327,6 +357,7 @@ TEMPLATE_TYPES = [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.COVER, Platform.IMAGE, Platform.NUMBER, Platform.SELECT, @@ -350,6 +381,11 @@ CONFIG_FLOW = { config_schema(Platform.BUTTON), validate_user_input=validate_user_input(Platform.BUTTON), ), + Platform.COVER: SchemaFlowFormStep( + config_schema(Platform.COVER), + preview="template", + validate_user_input=validate_user_input(Platform.COVER), + ), Platform.IMAGE: SchemaFlowFormStep( config_schema(Platform.IMAGE), preview="template", @@ -394,6 +430,11 @@ OPTIONS_FLOW = { options_schema(Platform.BUTTON), validate_user_input=validate_user_input(Platform.BUTTON), ), + Platform.COVER: SchemaFlowFormStep( + options_schema(Platform.COVER), + preview="template", + validate_user_input=validate_user_input(Platform.COVER), + ), Platform.IMAGE: SchemaFlowFormStep( options_schema(Platform.IMAGE), preview="template", @@ -427,6 +468,7 @@ CREATE_PREVIEW_ENTITY: dict[ ] = { Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel, Platform.BINARY_SENSOR: async_create_preview_binary_sensor, + Platform.COVER: async_create_preview_cover, Platform.NUMBER: async_create_preview_number, Platform.SELECT: async_create_preview_select, Platform.SENSOR: async_create_preview_sensor, diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index e8739fa8207..caac8cf5a1d 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -18,6 +18,7 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_COVERS, CONF_DEVICE_CLASS, @@ -31,14 +32,22 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import TriggerUpdateCoordinator from .const import DOMAIN from .entity import AbstractTemplateEntity -from .helpers import async_setup_template_platform +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) from .template_entity import ( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, @@ -91,23 +100,29 @@ LEGACY_FIELDS = { DEFAULT_NAME = "Template Cover" +COVER_COMMON_SCHEMA = vol.Schema( + { + vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, + vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_POSITION): cv.template, + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_TILT): cv.template, + vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, + } +) + COVER_YAML_SCHEMA = vol.All( vol.Schema( { - vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, - vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_POSITION): cv.template, - vol.Optional(CONF_STATE): cv.template, vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_TILT): cv.template, - vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, } ) - .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) - .extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA), + .extend(COVER_COMMON_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA) + .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema), cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), ) @@ -139,6 +154,11 @@ PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_LEGACY_YAML_SCHEMA)} ) +COVER_CONFIG_ENTRY_SCHEMA = vol.All( + COVER_COMMON_SCHEMA.extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema), + cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), +) + async def async_setup_platform( hass: HomeAssistant, @@ -160,6 +180,37 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + StateCoverEntity, + COVER_CONFIG_ENTRY_SCHEMA, + True, + ) + + +@callback +def async_create_preview_cover( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateCoverEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateCoverEntity, + COVER_CONFIG_ENTRY_SCHEMA, + True, + ) + + class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): """Representation of a template cover features.""" diff --git a/homeassistant/components/template/helpers.py b/homeassistant/components/template/helpers.py index 25f7011c794..a26b7bb0df1 100644 --- a/homeassistant/components/template/helpers.py +++ b/homeassistant/components/template/helpers.py @@ -242,7 +242,7 @@ async def async_setup_template_entry( config_entry: ConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, state_entity_cls: type[TemplateEntity], - config_schema: vol.Schema, + config_schema: vol.Schema | vol.All, replace_value_template: bool = False, ) -> None: """Setup the Template from a config entry.""" @@ -267,7 +267,7 @@ def async_setup_template_preview[T: TemplateEntity]( name: str, config: ConfigType, state_entity_cls: type[T], - schema: vol.Schema, + schema: vol.Schema | vol.All, replace_value_template: bool = False, ) -> T: """Setup the Template preview.""" diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index be91b27e485..36bca174ef6 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -80,6 +80,37 @@ }, "title": "Template button" }, + "cover": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "device_class": "[%key:component::template::common::device_class%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "open_cover": "Actions on open", + "close_cover": "Actions on close", + "stop_cover": "Actions on stop", + "position": "Position", + "set_cover_position": "Actions on set position" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the cover. Valid output values from the template are `open`, `opening`, `closing` and `closed` which are directly mapped to the corresponding states. If both a state and a position are specified, only `opening` and `closing` are set from the state template.", + "open_cover": "Defines actions to run when the cover is opened.", + "close_cover": "Defines actions to run when the cover is closed.", + "stop_cover": "Defines actions to run when the cover is stopped.", + "position": "Defines a template to get the position of the cover. Value values are numbers between `0` (`closed`) and `100` (`open`).", + "set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "Template cover" + }, "image": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -173,6 +204,7 @@ "alarm_control_panel": "Template an alarm control panel", "binary_sensor": "Template a binary sensor", "button": "Template a button", + "cover": "Template a cover", "image": "Template an image", "number": "Template a number", "select": "Template a select", @@ -270,6 +302,36 @@ }, "title": "[%key:component::template::config::step::button::title%]" }, + + "cover": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "state": "[%key:component::template::common::state%]", + "open_cover": "[%key:component::template::config::step::cover::data::open_cover%]", + "close_cover": "[%key:component::template::config::step::cover::data::close_cover%]", + "stop_cover": "[%key:component::template::config::step::cover::data::stop_cover%]", + "position": "[%key:component::template::config::step::cover::data::position%]", + "set_cover_position": "[%key:component::template::config::step::cover::data::set_cover_position%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::cover::data_description::state%]", + "open_cover": "[%key:component::template::config::step::cover::data_description::open_cover%]", + "close_cover": "[%key:component::template::config::step::cover::data_description::close_cover%]", + "stop_cover": "[%key:component::template::config::step::cover::data_description::stop_cover%]", + "position": "[%key:component::template::config::step::cover::data_description::position%]", + "set_cover_position": "[%key:component::template::config::step::cover::data_description::set_cover_position%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "[%key:component::template::config::step::cover::title%]" + }, "image": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -425,6 +487,20 @@ "update": "[%key:component::button::entity_component::update::name%]" } }, + "cover_device_class": { + "options": { + "awning": "[%key:component::cover::entity_component::awning::name%]", + "blind": "[%key:component::cover::entity_component::blind::name%]", + "curtain": "[%key:component::cover::entity_component::curtain::name%]", + "damper": "[%key:component::cover::entity_component::damper::name%]", + "door": "[%key:component::cover::entity_component::door::name%]", + "garage": "[%key:component::cover::entity_component::garage::name%]", + "gate": "[%key:component::cover::entity_component::gate::name%]", + "shade": "[%key:component::cover::entity_component::shade::name%]", + "shutter": "[%key:component::cover::entity_component::shutter::name%]", + "window": "[%key:component::cover::entity_component::window::name%]" + } + }, "sensor_device_class": { "options": { "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", diff --git a/tests/components/template/snapshots/test_cover.ambr b/tests/components/template/snapshots/test_cover.ambr new file mode 100644 index 00000000000..177dc8c883b --- /dev/null +++ b/tests/components/template/snapshots/test_cover.ambr @@ -0,0 +1,16 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 22acb1b2292..8d7f2e6d89c 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -121,6 +121,34 @@ BINARY_SENSOR_OPTIONS = { }, {}, ), + ( + "cover", + {"state": "{{ states('cover.one') }}"}, + "open", + {"one": "open", "two": "closed"}, + {}, + { + "device_class": "garage", + "set_cover_position": [ + { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"position": "{{ position }}"}, + } + ], + }, + { + "device_class": "garage", + "set_cover_position": [ + { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"position": "{{ position }}"}, + } + ], + }, + {}, + ), ( "image", {"url": "{{ states('sensor.one') }}"}, @@ -288,6 +316,12 @@ async def test_config_flow( {}, {}, ), + ( + "cover", + {"state": "{{ 'open' }}"}, + {"set_cover_position": []}, + {"set_cover_position": []}, + ), ( "image", { @@ -474,6 +508,16 @@ async def test_config_flow_device( }, "state", ), + ( + "cover", + {"state": "{{ states('cover.one') }}"}, + {"state": "{{ states('cover.two') }}"}, + ["open", "closed"], + {"one": "open", "two": "closed"}, + {"set_cover_position": []}, + {"set_cover_position": []}, + "state", + ), ( "image", { @@ -1315,6 +1359,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "cover", + {"state": "{{ states('cover.one') }}"}, + {"set_cover_position": []}, + {"set_cover_position": []}, + ), ( "image", { diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 48f45d879cd..dc3428330b0 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import cover, template from homeassistant.components.cover import ( @@ -32,9 +33,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import ConfigurationStyle +from .conftest import ConfigurationStyle, async_get_flow_preview_state -from tests.common import assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component +from tests.typing import WebSocketGenerator TEST_OBJECT_ID = "test_template_cover" TEST_ENTITY_ID = f"cover.{TEST_OBJECT_ID}" @@ -1604,3 +1606,52 @@ async def test_empty_action_config( state.attributes["supported_features"] == CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | supported_feature ) + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a cover from a config entry.""" + + hass.states.async_set( + "cover.test_state", + "open", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('cover.test_state') }}", + "set_cover_position": [], + "template_type": COVER_DOMAIN, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("cover.my_template") + assert state is not None + assert state == snapshot + + +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + cover.DOMAIN, + {"name": "My template", "state": "{{ 'open' }}", "set_cover_position": []}, + ) + + assert state["state"] == CoverState.OPEN From 1a75a88c76b94956ff2dbf50520a90573f5e2a84 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 30 Jul 2025 15:52:31 +0300 Subject: [PATCH 118/265] Add actions to Alexa Devices (#145645) --- .../components/alexa_devices/__init__.py | 13 +- .../components/alexa_devices/icons.json | 8 + .../components/alexa_devices/services.py | 121 ++++ .../components/alexa_devices/services.yaml | 504 +++++++++++++++++ .../components/alexa_devices/strings.json | 523 +++++++++++++++++- tests/components/alexa_devices/conftest.py | 1 + tests/components/alexa_devices/const.py | 2 + .../snapshots/test_services.ambr | 77 +++ .../components/alexa_devices/test_services.py | 195 +++++++ 9 files changed, 1442 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/alexa_devices/services.py create mode 100644 homeassistant/components/alexa_devices/services.yaml create mode 100644 tests/components/alexa_devices/snapshots/test_services.ambr create mode 100644 tests/components/alexa_devices/test_services.py diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index d18e730afcb..9df0e60850e 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -2,9 +2,12 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.typing import ConfigType +from .const import DOMAIN from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator +from .services import async_setup_services PLATFORMS = [ Platform.BINARY_SENSOR, @@ -13,6 +16,14 @@ PLATFORMS = [ Platform.SWITCH, ] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Alexa Devices component.""" + async_setup_services(hass) + return True + async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Set up Alexa Devices platform.""" diff --git a/homeassistant/components/alexa_devices/icons.json b/homeassistant/components/alexa_devices/icons.json index 492f89b8fe4..bedd4af1734 100644 --- a/homeassistant/components/alexa_devices/icons.json +++ b/homeassistant/components/alexa_devices/icons.json @@ -38,5 +38,13 @@ } } } + }, + "services": { + "send_sound": { + "service": "mdi:cast-audio" + }, + "send_text_command": { + "service": "mdi:microphone-message" + } } } diff --git a/homeassistant/components/alexa_devices/services.py b/homeassistant/components/alexa_devices/services.py new file mode 100644 index 00000000000..5463c7a4319 --- /dev/null +++ b/homeassistant/components/alexa_devices/services.py @@ -0,0 +1,121 @@ +"""Support for services.""" + +from aioamazondevices.sounds import SOUNDS_LIST +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import config_validation as cv, device_registry as dr + +from .const import DOMAIN +from .coordinator import AmazonConfigEntry + +ATTR_TEXT_COMMAND = "text_command" +ATTR_SOUND = "sound" +ATTR_SOUND_VARIANT = "sound_variant" +SERVICE_TEXT_COMMAND = "send_text_command" +SERVICE_SOUND_NOTIFICATION = "send_sound" + +SCHEMA_SOUND_SERVICE = vol.Schema( + { + vol.Required(ATTR_SOUND): cv.string, + vol.Required(ATTR_SOUND_VARIANT): cv.positive_int, + vol.Required(ATTR_DEVICE_ID): cv.string, + }, +) +SCHEMA_CUSTOM_COMMAND = vol.Schema( + { + vol.Required(ATTR_TEXT_COMMAND): cv.string, + vol.Required(ATTR_DEVICE_ID): cv.string, + } +) + + +@callback +def async_get_entry_id_for_service_call( + call: ServiceCall, +) -> tuple[dr.DeviceEntry, AmazonConfigEntry]: + """Get the entry ID related to a service call (by device ID).""" + device_registry = dr.async_get(call.hass) + device_id = call.data[ATTR_DEVICE_ID] + if (device_entry := device_registry.async_get(device_id)) is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_device_id", + translation_placeholders={"device_id": device_id}, + ) + + for entry_id in device_entry.config_entries: + if (entry := call.hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + if entry.state is not ConfigEntryState.LOADED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="entry_not_loaded", + translation_placeholders={"entry": entry.title}, + ) + return (device_entry, entry) + + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="config_entry_not_found", + translation_placeholders={"device_id": device_id}, + ) + + +async def _async_execute_action(call: ServiceCall, attribute: str) -> None: + """Execute action on the device.""" + device, config_entry = async_get_entry_id_for_service_call(call) + assert device.serial_number + value: str = call.data[attribute] + + coordinator = config_entry.runtime_data + + if attribute == ATTR_SOUND: + variant: int = call.data[ATTR_SOUND_VARIANT] + pad = "_" if variant > 10 else "_0" + file = f"{value}{pad}{variant!s}" + if value not in SOUNDS_LIST or variant > SOUNDS_LIST[value]: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_sound_value", + translation_placeholders={"sound": value, "variant": str(variant)}, + ) + await coordinator.api.call_alexa_sound( + coordinator.data[device.serial_number], file + ) + elif attribute == ATTR_TEXT_COMMAND: + await coordinator.api.call_alexa_text_command( + coordinator.data[device.serial_number], value + ) + + +async def async_send_sound_notification(call: ServiceCall) -> None: + """Send a sound notification to a AmazonDevice.""" + await _async_execute_action(call, ATTR_SOUND) + + +async def async_send_text_command(call: ServiceCall) -> None: + """Send a custom command to a AmazonDevice.""" + await _async_execute_action(call, ATTR_TEXT_COMMAND) + + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Set up the services for the Amazon Devices integration.""" + for service_name, method, schema in ( + ( + SERVICE_SOUND_NOTIFICATION, + async_send_sound_notification, + SCHEMA_SOUND_SERVICE, + ), + ( + SERVICE_TEXT_COMMAND, + async_send_text_command, + SCHEMA_CUSTOM_COMMAND, + ), + ): + hass.services.async_register(DOMAIN, service_name, method, schema=schema) diff --git a/homeassistant/components/alexa_devices/services.yaml b/homeassistant/components/alexa_devices/services.yaml new file mode 100644 index 00000000000..d9eef28aea2 --- /dev/null +++ b/homeassistant/components/alexa_devices/services.yaml @@ -0,0 +1,504 @@ +send_text_command: + fields: + device_id: + required: true + selector: + device: + integration: alexa_devices + text_command: + required: true + example: "Play B.B.C. on TuneIn" + selector: + text: + +send_sound: + fields: + device_id: + required: true + selector: + device: + integration: alexa_devices + sound_variant: + required: true + example: 1 + default: 1 + selector: + number: + min: 1 + max: 50 + sound: + required: true + example: amzn_sfx_doorbell_chime + default: amzn_sfx_doorbell_chime + selector: + select: + options: + - air_horn + - air_horns + - airboat + - airport + - aliens + - amzn_sfx_airplane_takeoff_whoosh + - amzn_sfx_army_march_clank_7x + - amzn_sfx_army_march_large_8x + - amzn_sfx_army_march_small_8x + - amzn_sfx_baby_big_cry + - amzn_sfx_baby_cry + - amzn_sfx_baby_fuss + - amzn_sfx_battle_group_clanks + - amzn_sfx_battle_man_grunts + - amzn_sfx_battle_men_grunts + - amzn_sfx_battle_men_horses + - amzn_sfx_battle_noisy_clanks + - amzn_sfx_battle_yells_men + - amzn_sfx_battle_yells_men_run + - amzn_sfx_bear_groan_roar + - amzn_sfx_bear_roar_grumble + - amzn_sfx_bear_roar_small + - amzn_sfx_beep_1x + - amzn_sfx_bell_med_chime + - amzn_sfx_bell_short_chime + - amzn_sfx_bell_timer + - amzn_sfx_bicycle_bell_ring + - amzn_sfx_bird_chickadee_chirp_1x + - amzn_sfx_bird_chickadee_chirps + - amzn_sfx_bird_forest + - amzn_sfx_bird_forest_short + - amzn_sfx_bird_robin_chirp_1x + - amzn_sfx_boing_long_1x + - amzn_sfx_boing_med_1x + - amzn_sfx_boing_short_1x + - amzn_sfx_bus_drive_past + - amzn_sfx_buzz_electronic + - amzn_sfx_buzzer_loud_alarm + - amzn_sfx_buzzer_small + - amzn_sfx_car_accelerate + - amzn_sfx_car_accelerate_noisy + - amzn_sfx_car_click_seatbelt + - amzn_sfx_car_close_door_1x + - amzn_sfx_car_drive_past + - amzn_sfx_car_honk_1x + - amzn_sfx_car_honk_2x + - amzn_sfx_car_honk_3x + - amzn_sfx_car_honk_long_1x + - amzn_sfx_car_into_driveway + - amzn_sfx_car_into_driveway_fast + - amzn_sfx_car_slam_door_1x + - amzn_sfx_car_undo_seatbelt + - amzn_sfx_cat_angry_meow_1x + - amzn_sfx_cat_angry_screech_1x + - amzn_sfx_cat_long_meow_1x + - amzn_sfx_cat_meow_1x + - amzn_sfx_cat_purr + - amzn_sfx_cat_purr_meow + - amzn_sfx_chicken_cluck + - amzn_sfx_church_bell_1x + - amzn_sfx_church_bells_ringing + - amzn_sfx_clear_throat_ahem + - amzn_sfx_clock_ticking + - amzn_sfx_clock_ticking_long + - amzn_sfx_copy_machine + - amzn_sfx_cough + - amzn_sfx_crow_caw_1x + - amzn_sfx_crowd_applause + - amzn_sfx_crowd_bar + - amzn_sfx_crowd_bar_rowdy + - amzn_sfx_crowd_boo + - amzn_sfx_crowd_cheer_med + - amzn_sfx_crowd_excited_cheer + - amzn_sfx_dog_med_bark_1x + - amzn_sfx_dog_med_bark_2x + - amzn_sfx_dog_med_bark_growl + - amzn_sfx_dog_med_growl_1x + - amzn_sfx_dog_med_woof_1x + - amzn_sfx_dog_small_bark_2x + - amzn_sfx_door_open + - amzn_sfx_door_shut + - amzn_sfx_doorbell + - amzn_sfx_doorbell_buzz + - amzn_sfx_doorbell_chime + - amzn_sfx_drinking_slurp + - amzn_sfx_drum_and_cymbal + - amzn_sfx_drum_comedy + - amzn_sfx_earthquake_rumble + - amzn_sfx_electric_guitar + - amzn_sfx_electronic_beep + - amzn_sfx_electronic_major_chord + - amzn_sfx_elephant + - amzn_sfx_elevator_bell_1x + - amzn_sfx_elevator_open_bell + - amzn_sfx_fairy_melodic_chimes + - amzn_sfx_fairy_sparkle_chimes + - amzn_sfx_faucet_drip + - amzn_sfx_faucet_running + - amzn_sfx_fireplace_crackle + - amzn_sfx_fireworks + - amzn_sfx_fireworks_firecrackers + - amzn_sfx_fireworks_launch + - amzn_sfx_fireworks_whistles + - amzn_sfx_food_frying + - amzn_sfx_footsteps + - amzn_sfx_footsteps_muffled + - amzn_sfx_ghost_spooky + - amzn_sfx_glass_on_table + - amzn_sfx_glasses_clink + - amzn_sfx_horse_gallop_4x + - amzn_sfx_horse_huff_whinny + - amzn_sfx_horse_neigh + - amzn_sfx_horse_neigh_low + - amzn_sfx_horse_whinny + - amzn_sfx_human_walking + - amzn_sfx_jar_on_table_1x + - amzn_sfx_kitchen_ambience + - amzn_sfx_large_crowd_cheer + - amzn_sfx_large_fire_crackling + - amzn_sfx_laughter + - amzn_sfx_laughter_giggle + - amzn_sfx_lightning_strike + - amzn_sfx_lion_roar + - amzn_sfx_magic_blast_1x + - amzn_sfx_monkey_calls_3x + - amzn_sfx_monkey_chimp + - amzn_sfx_monkeys_chatter + - amzn_sfx_motorcycle_accelerate + - amzn_sfx_motorcycle_engine_idle + - amzn_sfx_motorcycle_engine_rev + - amzn_sfx_musical_drone_intro + - amzn_sfx_oars_splashing_rowboat + - amzn_sfx_object_on_table_2x + - amzn_sfx_ocean_wave_1x + - amzn_sfx_ocean_wave_on_rocks_1x + - amzn_sfx_ocean_wave_surf + - amzn_sfx_people_walking + - amzn_sfx_person_running + - amzn_sfx_piano_note_1x + - amzn_sfx_punch + - amzn_sfx_rain + - amzn_sfx_rain_on_roof + - amzn_sfx_rain_thunder + - amzn_sfx_rat_squeak_2x + - amzn_sfx_rat_squeaks + - amzn_sfx_raven_caw_1x + - amzn_sfx_raven_caw_2x + - amzn_sfx_restaurant_ambience + - amzn_sfx_rooster_crow + - amzn_sfx_scifi_air_escaping + - amzn_sfx_scifi_alarm + - amzn_sfx_scifi_alien_voice + - amzn_sfx_scifi_boots_walking + - amzn_sfx_scifi_close_large_explosion + - amzn_sfx_scifi_door_open + - amzn_sfx_scifi_engines_on + - amzn_sfx_scifi_engines_on_large + - amzn_sfx_scifi_engines_on_short_burst + - amzn_sfx_scifi_explosion + - amzn_sfx_scifi_explosion_2x + - amzn_sfx_scifi_incoming_explosion + - amzn_sfx_scifi_laser_gun_battle + - amzn_sfx_scifi_laser_gun_fires + - amzn_sfx_scifi_laser_gun_fires_large + - amzn_sfx_scifi_long_explosion_1x + - amzn_sfx_scifi_missile + - amzn_sfx_scifi_motor_short_1x + - amzn_sfx_scifi_open_airlock + - amzn_sfx_scifi_radar_high_ping + - amzn_sfx_scifi_radar_low + - amzn_sfx_scifi_radar_medium + - amzn_sfx_scifi_run_away + - amzn_sfx_scifi_sheilds_up + - amzn_sfx_scifi_short_low_explosion + - amzn_sfx_scifi_small_whoosh_flyby + - amzn_sfx_scifi_small_zoom_flyby + - amzn_sfx_scifi_sonar_ping_3x + - amzn_sfx_scifi_sonar_ping_4x + - amzn_sfx_scifi_spaceship_flyby + - amzn_sfx_scifi_timer_beep + - amzn_sfx_scifi_zap_backwards + - amzn_sfx_scifi_zap_electric + - amzn_sfx_sheep_baa + - amzn_sfx_sheep_bleat + - amzn_sfx_silverware_clank + - amzn_sfx_sirens + - amzn_sfx_sleigh_bells + - amzn_sfx_small_stream + - amzn_sfx_sneeze + - amzn_sfx_stream + - amzn_sfx_strong_wind_desert + - amzn_sfx_strong_wind_whistling + - amzn_sfx_subway_leaving + - amzn_sfx_subway_passing + - amzn_sfx_subway_stopping + - amzn_sfx_swoosh_cartoon_fast + - amzn_sfx_swoosh_fast_1x + - amzn_sfx_swoosh_fast_6x + - amzn_sfx_test_tone + - amzn_sfx_thunder_rumble + - amzn_sfx_toilet_flush + - amzn_sfx_trumpet_bugle + - amzn_sfx_turkey_gobbling + - amzn_sfx_typing_medium + - amzn_sfx_typing_short + - amzn_sfx_typing_typewriter + - amzn_sfx_vacuum_off + - amzn_sfx_vacuum_on + - amzn_sfx_walking_in_mud + - amzn_sfx_walking_in_snow + - amzn_sfx_walking_on_grass + - amzn_sfx_water_dripping + - amzn_sfx_water_droplets + - amzn_sfx_wind_strong_gusting + - amzn_sfx_wind_whistling_desert + - amzn_sfx_wings_flap_4x + - amzn_sfx_wings_flap_fast + - amzn_sfx_wolf_howl + - amzn_sfx_wolf_young_howl + - amzn_sfx_wooden_door + - amzn_sfx_wooden_door_creaks_long + - amzn_sfx_wooden_door_creaks_multiple + - amzn_sfx_wooden_door_creaks_open + - amzn_ui_sfx_gameshow_bridge + - amzn_ui_sfx_gameshow_countdown_loop_32s_full + - amzn_ui_sfx_gameshow_countdown_loop_64s_full + - amzn_ui_sfx_gameshow_countdown_loop_64s_minimal + - amzn_ui_sfx_gameshow_intro + - amzn_ui_sfx_gameshow_negative_response + - amzn_ui_sfx_gameshow_neutral_response + - amzn_ui_sfx_gameshow_outro + - amzn_ui_sfx_gameshow_player1 + - amzn_ui_sfx_gameshow_player2 + - amzn_ui_sfx_gameshow_player3 + - amzn_ui_sfx_gameshow_player4 + - amzn_ui_sfx_gameshow_positive_response + - amzn_ui_sfx_gameshow_tally_negative + - amzn_ui_sfx_gameshow_tally_positive + - amzn_ui_sfx_gameshow_waiting_loop_30s + - anchor + - answering_machines + - arcs_sparks + - arrows_bows + - baby + - back_up_beeps + - bars_restaurants + - baseball + - basketball + - battles + - beeps_tones + - bell + - bikes + - billiards + - board_games + - body + - boing + - books + - bow_wash + - box + - break_shatter_smash + - breaks + - brooms_mops + - bullets + - buses + - buzz + - buzz_hums + - buzzers + - buzzers_pistols + - cables_metal + - camera + - cannons + - car_alarm + - car_alarms + - car_cell_phones + - carnivals_fairs + - cars + - casino + - casinos + - cellar + - chimes + - chimes_bells + - chorus + - christmas + - church_bells + - clock + - cloth + - concrete + - construction + - construction_factory + - crashes + - crowds + - debris + - dining_kitchens + - dinosaurs + - dripping + - drops + - electric + - electrical + - elevator + - evolution_monsters + - explosions + - factory + - falls + - fax_scanner_copier + - feedback_mics + - fight + - fire + - fire_extinguisher + - fireballs + - fireworks + - fishing_pole + - flags + - football + - footsteps + - futuristic + - futuristic_ship + - gameshow + - gear + - ghosts_demons + - giant_monster + - glass + - glasses_clink + - golf + - gorilla + - grenade_lanucher + - griffen + - gyms_locker_rooms + - handgun_loading + - handgun_shot + - handle + - hands + - heartbeats_ekg + - helicopter + - high_tech + - hit_punch_slap + - hits + - horns + - horror + - hot_tub_filling_up + - human + - human_vocals + - hygene # codespell:ignore + - ice_skating + - ignitions + - infantry + - intro + - jet + - juggling + - key_lock + - kids + - knocks + - lab_equip + - lacrosse + - lamps_lanterns + - leather + - liquid_suction + - locker_doors + - machine_gun + - magic_spells + - medium_large_explosions + - metal + - modern_rings + - money_coins + - motorcycles + - movement + - moves + - nature + - oar_boat + - pagers + - paintball + - paper + - parachute + - pay_phones + - phone_beeps + - pigmy_bats + - pills + - pour_water + - power_up_down + - printers + - prison + - public_space + - racquetball + - radios_static + - rain + - rc_airplane + - rc_car + - refrigerators_freezers + - regular + - respirator + - rifle + - roller_coaster + - rollerskates_rollerblades + - room_tones + - ropes_climbing + - rotary_rings + - rowboat_canoe + - rubber + - running + - sails + - sand_gravel + - screen_doors + - screens + - seats_stools + - servos + - shoes_boots + - shotgun + - shower + - sink_faucet + - sink_filling_water + - sink_run_and_off + - sink_water_splatter + - sirens + - skateboards + - ski + - skids_tires + - sled + - slides + - small_explosions + - snow + - snowmobile + - soldiers + - splash_water + - splashes_sprays + - sports_whistles + - squeaks + - squeaky + - stairs + - steam + - submarine_diesel + - swing_doors + - switches_levers + - swords + - tape + - tape_machine + - televisions_shows + - tennis_pingpong + - textile + - throw + - thunder + - ticks + - timer + - toilet_flush + - tone + - tones_noises + - toys + - tractors + - traffic + - train + - trucks_vans + - turnstiles + - typing + - umbrella + - underwater + - vampires + - various + - video_tunes + - volcano_earthquake + - watches + - water + - water_running + - werewolves + - winches_gears + - wind + - wood + - wood_boat + - woosh + - zap + - zippers + translation_key: sound diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index 19cc39cab42..1b1150d5649 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -4,7 +4,8 @@ "data_description_country": "The country where your Amazon account is registered.", "data_description_username": "The email address of your Amazon account.", "data_description_password": "The password of your Amazon account.", - "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported." + "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.", + "device_id_description": "The ID of the device to send the command to." }, "config": { "flow_title": "{username}", @@ -84,12 +85,532 @@ } } }, + "services": { + "send_sound": { + "name": "Send sound", + "description": "Sends a sound to a device", + "fields": { + "device_id": { + "name": "Device", + "description": "[%key:component::alexa_devices::common::device_id_description%]" + }, + "sound": { + "name": "Alexa Skill sound file", + "description": "The sound file to play." + }, + "sound_variant": { + "name": "Sound variant", + "description": "The variant of the sound to play." + } + } + }, + "send_text_command": { + "name": "Send text command", + "description": "Sends a text command to a device", + "fields": { + "text_command": { + "name": "Alexa text command", + "description": "The text command to send." + }, + "device_id": { + "name": "Device", + "description": "[%key:component::alexa_devices::common::device_id_description%]" + } + } + } + }, + "selector": { + "sound": { + "options": { + "air_horn": "Air Horn", + "air_horns": "Air Horns", + "airboat": "Airboat", + "airport": "Airport", + "aliens": "Aliens", + "amzn_sfx_airplane_takeoff_whoosh": "Airplane Takeoff Whoosh", + "amzn_sfx_army_march_clank_7x": "Army March Clank 7x", + "amzn_sfx_army_march_large_8x": "Army March Large 8x", + "amzn_sfx_army_march_small_8x": "Army March Small 8x", + "amzn_sfx_baby_big_cry": "Baby Big Cry", + "amzn_sfx_baby_cry": "Baby Cry", + "amzn_sfx_baby_fuss": "Baby Fuss", + "amzn_sfx_battle_group_clanks": "Battle Group Clanks", + "amzn_sfx_battle_man_grunts": "Battle Man Grunts", + "amzn_sfx_battle_men_grunts": "Battle Men Grunts", + "amzn_sfx_battle_men_horses": "Battle Men Horses", + "amzn_sfx_battle_noisy_clanks": "Battle Noisy Clanks", + "amzn_sfx_battle_yells_men": "Battle Yells Men", + "amzn_sfx_battle_yells_men_run": "Battle Yells Men Run", + "amzn_sfx_bear_groan_roar": "Bear Groan Roar", + "amzn_sfx_bear_roar_grumble": "Bear Roar Grumble", + "amzn_sfx_bear_roar_small": "Bear Roar Small", + "amzn_sfx_beep_1x": "Beep 1x", + "amzn_sfx_bell_med_chime": "Bell Med Chime", + "amzn_sfx_bell_short_chime": "Bell Short Chime", + "amzn_sfx_bell_timer": "Bell Timer", + "amzn_sfx_bicycle_bell_ring": "Bicycle Bell Ring", + "amzn_sfx_bird_chickadee_chirp_1x": "Bird Chickadee Chirp 1x", + "amzn_sfx_bird_chickadee_chirps": "Bird Chickadee Chirps", + "amzn_sfx_bird_forest": "Bird Forest", + "amzn_sfx_bird_forest_short": "Bird Forest Short", + "amzn_sfx_bird_robin_chirp_1x": "Bird Robin Chirp 1x", + "amzn_sfx_boing_long_1x": "Boing Long 1x", + "amzn_sfx_boing_med_1x": "Boing Med 1x", + "amzn_sfx_boing_short_1x": "Boing Short 1x", + "amzn_sfx_bus_drive_past": "Bus Drive Past", + "amzn_sfx_buzz_electronic": "Buzz Electronic", + "amzn_sfx_buzzer_loud_alarm": "Buzzer Loud Alarm", + "amzn_sfx_buzzer_small": "Buzzer Small", + "amzn_sfx_car_accelerate": "Car Accelerate", + "amzn_sfx_car_accelerate_noisy": "Car Accelerate Noisy", + "amzn_sfx_car_click_seatbelt": "Car Click Seatbelt", + "amzn_sfx_car_close_door_1x": "Car Close Door 1x", + "amzn_sfx_car_drive_past": "Car Drive Past", + "amzn_sfx_car_honk_1x": "Car Honk 1x", + "amzn_sfx_car_honk_2x": "Car Honk 2x", + "amzn_sfx_car_honk_3x": "Car Honk 3x", + "amzn_sfx_car_honk_long_1x": "Car Honk Long 1x", + "amzn_sfx_car_into_driveway": "Car Into Driveway", + "amzn_sfx_car_into_driveway_fast": "Car Into Driveway Fast", + "amzn_sfx_car_slam_door_1x": "Car Slam Door 1x", + "amzn_sfx_car_undo_seatbelt": "Car Undo Seatbelt", + "amzn_sfx_cat_angry_meow_1x": "Cat Angry Meow 1x", + "amzn_sfx_cat_angry_screech_1x": "Cat Angry Screech 1x", + "amzn_sfx_cat_long_meow_1x": "Cat Long Meow 1x", + "amzn_sfx_cat_meow_1x": "Cat Meow 1x", + "amzn_sfx_cat_purr": "Cat Purr", + "amzn_sfx_cat_purr_meow": "Cat Purr Meow", + "amzn_sfx_chicken_cluck": "Chicken Cluck", + "amzn_sfx_church_bell_1x": "Church Bell 1x", + "amzn_sfx_church_bells_ringing": "Church Bells Ringing", + "amzn_sfx_clear_throat_ahem": "Clear Throat Ahem", + "amzn_sfx_clock_ticking": "Clock Ticking", + "amzn_sfx_clock_ticking_long": "Clock Ticking Long", + "amzn_sfx_copy_machine": "Copy Machine", + "amzn_sfx_cough": "Cough", + "amzn_sfx_crow_caw_1x": "Crow Caw 1x", + "amzn_sfx_crowd_applause": "Crowd Applause", + "amzn_sfx_crowd_bar": "Crowd Bar", + "amzn_sfx_crowd_bar_rowdy": "Crowd Bar Rowdy", + "amzn_sfx_crowd_boo": "Crowd Boo", + "amzn_sfx_crowd_cheer_med": "Crowd Cheer Med", + "amzn_sfx_crowd_excited_cheer": "Crowd Excited Cheer", + "amzn_sfx_dog_med_bark_1x": "Dog Med Bark 1x", + "amzn_sfx_dog_med_bark_2x": "Dog Med Bark 2x", + "amzn_sfx_dog_med_bark_growl": "Dog Med Bark Growl", + "amzn_sfx_dog_med_growl_1x": "Dog Med Growl 1x", + "amzn_sfx_dog_med_woof_1x": "Dog Med Woof 1x", + "amzn_sfx_dog_small_bark_2x": "Dog Small Bark 2x", + "amzn_sfx_door_open": "Door Open", + "amzn_sfx_door_shut": "Door Shut", + "amzn_sfx_doorbell": "Doorbell", + "amzn_sfx_doorbell_buzz": "Doorbell Buzz", + "amzn_sfx_doorbell_chime": "Doorbell Chime", + "amzn_sfx_drinking_slurp": "Drinking Slurp", + "amzn_sfx_drum_and_cymbal": "Drum And Cymbal", + "amzn_sfx_drum_comedy": "Drum Comedy", + "amzn_sfx_earthquake_rumble": "Earthquake Rumble", + "amzn_sfx_electric_guitar": "Electric Guitar", + "amzn_sfx_electronic_beep": "Electronic Beep", + "amzn_sfx_electronic_major_chord": "Electronic Major Chord", + "amzn_sfx_elephant": "Elephant", + "amzn_sfx_elevator_bell_1x": "Elevator Bell 1x", + "amzn_sfx_elevator_open_bell": "Elevator Open Bell", + "amzn_sfx_fairy_melodic_chimes": "Fairy Melodic Chimes", + "amzn_sfx_fairy_sparkle_chimes": "Fairy Sparkle Chimes", + "amzn_sfx_faucet_drip": "Faucet Drip", + "amzn_sfx_faucet_running": "Faucet Running", + "amzn_sfx_fireplace_crackle": "Fireplace Crackle", + "amzn_sfx_fireworks": "Fireworks", + "amzn_sfx_fireworks_firecrackers": "Fireworks Firecrackers", + "amzn_sfx_fireworks_launch": "Fireworks Launch", + "amzn_sfx_fireworks_whistles": "Fireworks Whistles", + "amzn_sfx_food_frying": "Food Frying", + "amzn_sfx_footsteps": "Footsteps", + "amzn_sfx_footsteps_muffled": "Footsteps Muffled", + "amzn_sfx_ghost_spooky": "Ghost Spooky", + "amzn_sfx_glass_on_table": "Glass On Table", + "amzn_sfx_glasses_clink": "Glasses Clink", + "amzn_sfx_horse_gallop_4x": "Horse Gallop 4x", + "amzn_sfx_horse_huff_whinny": "Horse Huff Whinny", + "amzn_sfx_horse_neigh": "Horse Neigh", + "amzn_sfx_horse_neigh_low": "Horse Neigh Low", + "amzn_sfx_horse_whinny": "Horse Whinny", + "amzn_sfx_human_walking": "Human Walking", + "amzn_sfx_jar_on_table_1x": "Jar On Table 1x", + "amzn_sfx_kitchen_ambience": "Kitchen Ambience", + "amzn_sfx_large_crowd_cheer": "Large Crowd Cheer", + "amzn_sfx_large_fire_crackling": "Large Fire Crackling", + "amzn_sfx_laughter": "Laughter", + "amzn_sfx_laughter_giggle": "Laughter Giggle", + "amzn_sfx_lightning_strike": "Lightning Strike", + "amzn_sfx_lion_roar": "Lion Roar", + "amzn_sfx_magic_blast_1x": "Magic Blast 1x", + "amzn_sfx_monkey_calls_3x": "Monkey Calls 3x", + "amzn_sfx_monkey_chimp": "Monkey Chimp", + "amzn_sfx_monkeys_chatter": "Monkeys Chatter", + "amzn_sfx_motorcycle_accelerate": "Motorcycle Accelerate", + "amzn_sfx_motorcycle_engine_idle": "Motorcycle Engine Idle", + "amzn_sfx_motorcycle_engine_rev": "Motorcycle Engine Rev", + "amzn_sfx_musical_drone_intro": "Musical Drone Intro", + "amzn_sfx_oars_splashing_rowboat": "Oars Splashing Rowboat", + "amzn_sfx_object_on_table_2x": "Object On Table 2x", + "amzn_sfx_ocean_wave_1x": "Ocean Wave 1x", + "amzn_sfx_ocean_wave_on_rocks_1x": "Ocean Wave On Rocks 1x", + "amzn_sfx_ocean_wave_surf": "Ocean Wave Surf", + "amzn_sfx_people_walking": "People Walking", + "amzn_sfx_person_running": "Person Running", + "amzn_sfx_piano_note_1x": "Piano Note 1x", + "amzn_sfx_punch": "Punch", + "amzn_sfx_rain": "Rain", + "amzn_sfx_rain_on_roof": "Rain On Roof", + "amzn_sfx_rain_thunder": "Rain Thunder", + "amzn_sfx_rat_squeak_2x": "Rat Squeak 2x", + "amzn_sfx_rat_squeaks": "Rat Squeaks", + "amzn_sfx_raven_caw_1x": "Raven Caw 1x", + "amzn_sfx_raven_caw_2x": "Raven Caw 2x", + "amzn_sfx_restaurant_ambience": "Restaurant Ambience", + "amzn_sfx_rooster_crow": "Rooster Crow", + "amzn_sfx_scifi_air_escaping": "Scifi Air Escaping", + "amzn_sfx_scifi_alarm": "Scifi Alarm", + "amzn_sfx_scifi_alien_voice": "Scifi Alien Voice", + "amzn_sfx_scifi_boots_walking": "Scifi Boots Walking", + "amzn_sfx_scifi_close_large_explosion": "Scifi Close Large Explosion", + "amzn_sfx_scifi_door_open": "Scifi Door Open", + "amzn_sfx_scifi_engines_on": "Scifi Engines On", + "amzn_sfx_scifi_engines_on_large": "Scifi Engines On Large", + "amzn_sfx_scifi_engines_on_short_burst": "Scifi Engines On Short Burst", + "amzn_sfx_scifi_explosion": "Scifi Explosion", + "amzn_sfx_scifi_explosion_2x": "Scifi Explosion 2x", + "amzn_sfx_scifi_incoming_explosion": "Scifi Incoming Explosion", + "amzn_sfx_scifi_laser_gun_battle": "Scifi Laser Gun Battle", + "amzn_sfx_scifi_laser_gun_fires": "Scifi Laser Gun Fires", + "amzn_sfx_scifi_laser_gun_fires_large": "Scifi Laser Gun Fires Large", + "amzn_sfx_scifi_long_explosion_1x": "Scifi Long Explosion 1x", + "amzn_sfx_scifi_missile": "Scifi Missile", + "amzn_sfx_scifi_motor_short_1x": "Scifi Motor Short 1x", + "amzn_sfx_scifi_open_airlock": "Scifi Open Airlock", + "amzn_sfx_scifi_radar_high_ping": "Scifi Radar High Ping", + "amzn_sfx_scifi_radar_low": "Scifi Radar Low", + "amzn_sfx_scifi_radar_medium": "Scifi Radar Medium", + "amzn_sfx_scifi_run_away": "Scifi Run Away", + "amzn_sfx_scifi_sheilds_up": "Scifi Sheilds Up", + "amzn_sfx_scifi_short_low_explosion": "Scifi Short Low Explosion", + "amzn_sfx_scifi_small_whoosh_flyby": "Scifi Small Whoosh Flyby", + "amzn_sfx_scifi_small_zoom_flyby": "Scifi Small Zoom Flyby", + "amzn_sfx_scifi_sonar_ping_3x": "Scifi Sonar Ping 3x", + "amzn_sfx_scifi_sonar_ping_4x": "Scifi Sonar Ping 4x", + "amzn_sfx_scifi_spaceship_flyby": "Scifi Spaceship Flyby", + "amzn_sfx_scifi_timer_beep": "Scifi Timer Beep", + "amzn_sfx_scifi_zap_backwards": "Scifi Zap Backwards", + "amzn_sfx_scifi_zap_electric": "Scifi Zap Electric", + "amzn_sfx_sheep_baa": "Sheep Baa", + "amzn_sfx_sheep_bleat": "Sheep Bleat", + "amzn_sfx_silverware_clank": "Silverware Clank", + "amzn_sfx_sirens": "Sirens", + "amzn_sfx_sleigh_bells": "Sleigh Bells", + "amzn_sfx_small_stream": "Small Stream", + "amzn_sfx_sneeze": "Sneeze", + "amzn_sfx_stream": "Stream", + "amzn_sfx_strong_wind_desert": "Strong Wind Desert", + "amzn_sfx_strong_wind_whistling": "Strong Wind Whistling", + "amzn_sfx_subway_leaving": "Subway Leaving", + "amzn_sfx_subway_passing": "Subway Passing", + "amzn_sfx_subway_stopping": "Subway Stopping", + "amzn_sfx_swoosh_cartoon_fast": "Swoosh Cartoon Fast", + "amzn_sfx_swoosh_fast_1x": "Swoosh Fast 1x", + "amzn_sfx_swoosh_fast_6x": "Swoosh Fast 6x", + "amzn_sfx_test_tone": "Test Tone", + "amzn_sfx_thunder_rumble": "Thunder Rumble", + "amzn_sfx_toilet_flush": "Toilet Flush", + "amzn_sfx_trumpet_bugle": "Trumpet Bugle", + "amzn_sfx_turkey_gobbling": "Turkey Gobbling", + "amzn_sfx_typing_medium": "Typing Medium", + "amzn_sfx_typing_short": "Typing Short", + "amzn_sfx_typing_typewriter": "Typing Typewriter", + "amzn_sfx_vacuum_off": "Vacuum Off", + "amzn_sfx_vacuum_on": "Vacuum On", + "amzn_sfx_walking_in_mud": "Walking In Mud", + "amzn_sfx_walking_in_snow": "Walking In Snow", + "amzn_sfx_walking_on_grass": "Walking On Grass", + "amzn_sfx_water_dripping": "Water Dripping", + "amzn_sfx_water_droplets": "Water Droplets", + "amzn_sfx_wind_strong_gusting": "Wind Strong Gusting", + "amzn_sfx_wind_whistling_desert": "Wind Whistling Desert", + "amzn_sfx_wings_flap_4x": "Wings Flap 4x", + "amzn_sfx_wings_flap_fast": "Wings Flap Fast", + "amzn_sfx_wolf_howl": "Wolf Howl", + "amzn_sfx_wolf_young_howl": "Wolf Young Howl", + "amzn_sfx_wooden_door": "Wooden Door", + "amzn_sfx_wooden_door_creaks_long": "Wooden Door Creaks Long", + "amzn_sfx_wooden_door_creaks_multiple": "Wooden Door Creaks Multiple", + "amzn_sfx_wooden_door_creaks_open": "Wooden Door Creaks Open", + "amzn_ui_sfx_gameshow_bridge": "Gameshow Bridge", + "amzn_ui_sfx_gameshow_countdown_loop_32s_full": "Gameshow Countdown Loop 32s Full", + "amzn_ui_sfx_gameshow_countdown_loop_64s_full": "Gameshow Countdown Loop 64s Full", + "amzn_ui_sfx_gameshow_countdown_loop_64s_minimal": "Gameshow Countdown Loop 64s Minimal", + "amzn_ui_sfx_gameshow_intro": "Gameshow Intro", + "amzn_ui_sfx_gameshow_negative_response": "Gameshow Negative Response", + "amzn_ui_sfx_gameshow_neutral_response": "Gameshow Neutral Response", + "amzn_ui_sfx_gameshow_outro": "Gameshow Outro", + "amzn_ui_sfx_gameshow_player1": "Gameshow Player1", + "amzn_ui_sfx_gameshow_player2": "Gameshow Player2", + "amzn_ui_sfx_gameshow_player3": "Gameshow Player3", + "amzn_ui_sfx_gameshow_player4": "Gameshow Player4", + "amzn_ui_sfx_gameshow_positive_response": "Gameshow Positive Response", + "amzn_ui_sfx_gameshow_tally_negative": "Gameshow Tally Negative", + "amzn_ui_sfx_gameshow_tally_positive": "Gameshow Tally Positive", + "amzn_ui_sfx_gameshow_waiting_loop_30s": "Gameshow Waiting Loop 30s", + "anchor": "Anchor", + "answering_machines": "Answering Machines", + "arcs_sparks": "Arcs Sparks", + "arrows_bows": "Arrows Bows", + "baby": "Baby", + "back_up_beeps": "Back Up Beeps", + "bars_restaurants": "Bars Restaurants", + "baseball": "Baseball", + "basketball": "Basketball", + "battles": "Battles", + "beeps_tones": "Beeps Tones", + "bell": "Bell", + "bikes": "Bikes", + "billiards": "Billiards", + "board_games": "Board Games", + "body": "Body", + "boing": "Boing", + "books": "Books", + "bow_wash": "Bow Wash", + "box": "Box", + "break_shatter_smash": "Break Shatter Smash", + "breaks": "Breaks", + "brooms_mops": "Brooms Mops", + "bullets": "Bullets", + "buses": "Buses", + "buzz": "Buzz", + "buzz_hums": "Buzz Hums", + "buzzers": "Buzzers", + "buzzers_pistols": "Buzzers Pistols", + "cables_metal": "Cables Metal", + "camera": "Camera", + "cannons": "Cannons", + "car_alarm": "Car Alarm", + "car_alarms": "Car Alarms", + "car_cell_phones": "Car Cell Phones", + "carnivals_fairs": "Carnivals Fairs", + "cars": "Cars", + "casino": "Casino", + "casinos": "Casinos", + "cellar": "Cellar", + "chimes": "Chimes", + "chimes_bells": "Chimes Bells", + "chorus": "Chorus", + "christmas": "Christmas", + "church_bells": "Church Bells", + "clock": "Clock", + "cloth": "Cloth", + "concrete": "Concrete", + "construction": "Construction", + "construction_factory": "Construction Factory", + "crashes": "Crashes", + "crowds": "Crowds", + "debris": "Debris", + "dining_kitchens": "Dining Kitchens", + "dinosaurs": "Dinosaurs", + "dripping": "Dripping", + "drops": "Drops", + "electric": "Electric", + "electrical": "Electrical", + "elevator": "Elevator", + "evolution_monsters": "Evolution Monsters", + "explosions": "Explosions", + "factory": "Factory", + "falls": "Falls", + "fax_scanner_copier": "Fax Scanner Copier", + "feedback_mics": "Feedback Mics", + "fight": "Fight", + "fire": "Fire", + "fire_extinguisher": "Fire Extinguisher", + "fireballs": "Fireballs", + "fireworks": "Fireworks", + "fishing_pole": "Fishing Pole", + "flags": "Flags", + "football": "Football", + "footsteps": "Footsteps", + "futuristic": "Futuristic", + "futuristic_ship": "Futuristic Ship", + "gameshow": "Gameshow", + "gear": "Gear", + "ghosts_demons": "Ghosts Demons", + "giant_monster": "Giant Monster", + "glass": "Glass", + "glasses_clink": "Glasses Clink", + "golf": "Golf", + "gorilla": "Gorilla", + "grenade_lanucher": "Grenade Lanucher", + "griffen": "Griffen", + "gyms_locker_rooms": "Gyms Locker Rooms", + "handgun_loading": "Handgun Loading", + "handgun_shot": "Handgun Shot", + "handle": "Handle", + "hands": "Hands", + "heartbeats_ekg": "Heartbeats EKG", + "helicopter": "Helicopter", + "high_tech": "High Tech", + "hit_punch_slap": "Hit Punch Slap", + "hits": "Hits", + "horns": "Horns", + "horror": "Horror", + "hot_tub_filling_up": "Hot Tub Filling Up", + "human": "Human", + "human_vocals": "Human Vocals", + "hygene": "Hygene", + "ice_skating": "Ice Skating", + "ignitions": "Ignitions", + "infantry": "Infantry", + "intro": "Intro", + "jet": "Jet", + "juggling": "Juggling", + "key_lock": "Key Lock", + "kids": "Kids", + "knocks": "Knocks", + "lab_equip": "Lab Equip", + "lacrosse": "Lacrosse", + "lamps_lanterns": "Lamps Lanterns", + "leather": "Leather", + "liquid_suction": "Liquid Suction", + "locker_doors": "Locker Doors", + "machine_gun": "Machine Gun", + "magic_spells": "Magic Spells", + "medium_large_explosions": "Medium Large Explosions", + "metal": "Metal", + "modern_rings": "Modern Rings", + "money_coins": "Money Coins", + "motorcycles": "Motorcycles", + "movement": "Movement", + "moves": "Moves", + "nature": "Nature", + "oar_boat": "Oar Boat", + "pagers": "Pagers", + "paintball": "Paintball", + "paper": "Paper", + "parachute": "Parachute", + "pay_phones": "Pay Phones", + "phone_beeps": "Phone Beeps", + "pigmy_bats": "Pigmy Bats", + "pills": "Pills", + "pour_water": "Pour Water", + "power_up_down": "Power Up Down", + "printers": "Printers", + "prison": "Prison", + "public_space": "Public Space", + "racquetball": "Racquetball", + "radios_static": "Radios Static", + "rain": "Rain", + "rc_airplane": "RC Airplane", + "rc_car": "RC Car", + "refrigerators_freezers": "Refrigerators Freezers", + "regular": "Regular", + "respirator": "Respirator", + "rifle": "Rifle", + "roller_coaster": "Roller Coaster", + "rollerskates_rollerblades": "RollerSkates RollerBlades", + "room_tones": "Room Tones", + "ropes_climbing": "Ropes Climbing", + "rotary_rings": "Rotary Rings", + "rowboat_canoe": "Rowboat Canoe", + "rubber": "Rubber", + "running": "Running", + "sails": "Sails", + "sand_gravel": "Sand Gravel", + "screen_doors": "Screen Doors", + "screens": "Screens", + "seats_stools": "Seats Stools", + "servos": "Servos", + "shoes_boots": "Shoes Boots", + "shotgun": "Shotgun", + "shower": "Shower", + "sink_faucet": "Sink Faucet", + "sink_filling_water": "Sink Filling Water", + "sink_run_and_off": "Sink Run And Off", + "sink_water_splatter": "Sink Water Splatter", + "sirens": "Sirens", + "skateboards": "Skateboards", + "ski": "Ski", + "skids_tires": "Skids Tires", + "sled": "Sled", + "slides": "Slides", + "small_explosions": "Small Explosions", + "snow": "Snow", + "snowmobile": "Snowmobile", + "soldiers": "Soldiers", + "splash_water": "Splash Water", + "splashes_sprays": "Splashes Sprays", + "sports_whistles": "Sports Whistles", + "squeaks": "Squeaks", + "squeaky": "Squeaky", + "stairs": "Stairs", + "steam": "Steam", + "submarine_diesel": "Submarine Diesel", + "swing_doors": "Swing Doors", + "switches_levers": "Switches Levers", + "swords": "Swords", + "tape": "Tape", + "tape_machine": "Tape Machine", + "televisions_shows": "Televisions Shows", + "tennis_pingpong": "Tennis PingPong", + "textile": "Textile", + "throw": "Throw", + "thunder": "Thunder", + "ticks": "Ticks", + "timer": "Timer", + "toilet_flush": "Toilet Flush", + "tone": "Tone", + "tones_noises": "Tones Noises", + "toys": "Toys", + "tractors": "Tractors", + "traffic": "Traffic", + "train": "Train", + "trucks_vans": "Trucks Vans", + "turnstiles": "Turnstiles", + "typing": "Typing", + "umbrella": "Umbrella", + "underwater": "Underwater", + "vampires": "Vampires", + "various": "Various", + "video_tunes": "Video Tunes", + "volcano_earthquake": "Volcano Earthquake", + "watches": "Watches", + "water": "Water", + "water_running": "Water Running", + "werewolves": "Werewolves", + "winches_gears": "Winches Gears", + "wind": "Wind", + "wood": "Wood", + "wood_boat": "Wood Boat", + "woosh": "Woosh", + "zap": "Zap", + "zippers": "Zippers" + } + } + }, "exceptions": { "cannot_connect_with_error": { "message": "Error connecting: {error}" }, "cannot_retrieve_data_with_error": { "message": "Error retrieving data: {error}" + }, + "device_serial_number_missing": { + "message": "Device serial number missing: {device_id}" + }, + "invalid_device_id": { + "message": "Invalid device ID specified: {device_id}" + }, + "invalid_sound_value": { + "message": "Invalid sound {sound} with variant {variant} specified" + }, + "entry_not_loaded": { + "message": "Entry not loaded: {entry}" } } } diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index a5a49a343a9..22596706862 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -69,6 +69,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get( device.device_type ) + client.send_sound_notification = AsyncMock() yield client diff --git a/tests/components/alexa_devices/const.py b/tests/components/alexa_devices/const.py index 8a2f5b6b158..6a4dff1c38d 100644 --- a/tests/components/alexa_devices/const.py +++ b/tests/components/alexa_devices/const.py @@ -5,3 +5,5 @@ TEST_COUNTRY = "IT" TEST_PASSWORD = "fake_password" TEST_SERIAL_NUMBER = "echo_test_serial_number" TEST_USERNAME = "fake_email@gmail.com" + +TEST_DEVICE_ID = "echo_test_device_id" diff --git a/tests/components/alexa_devices/snapshots/test_services.ambr b/tests/components/alexa_devices/snapshots/test_services.ambr new file mode 100644 index 00000000000..b95108b0d03 --- /dev/null +++ b/tests/components/alexa_devices/snapshots/test_services.ambr @@ -0,0 +1,77 @@ +# serializer version: 1 +# name: test_send_sound_service + _Call( + tuple( + dict({ + 'account_name': 'Echo Test', + 'appliance_id': 'G1234567890123456789012345678A', + 'bluetooth_state': True, + 'capabilities': list([ + 'AUDIO_PLAYER', + 'MICROPHONE', + ]), + 'device_cluster_members': list([ + 'echo_test_serial_number', + ]), + 'device_family': 'mine', + 'device_locale': 'en-US', + 'device_owner_customer_id': 'amazon_ower_id', + 'device_type': 'echo', + 'do_not_disturb': False, + 'entity_id': '11111111-2222-3333-4444-555555555555', + 'online': True, + 'response_style': None, + 'sensors': dict({ + 'temperature': dict({ + 'name': 'temperature', + 'scale': 'CELSIUS', + 'value': '22.5', + }), + }), + 'serial_number': 'echo_test_serial_number', + 'software_version': 'echo_test_software_version', + }), + 'chimes_bells_01', + ), + dict({ + }), + ) +# --- +# name: test_send_text_service + _Call( + tuple( + dict({ + 'account_name': 'Echo Test', + 'appliance_id': 'G1234567890123456789012345678A', + 'bluetooth_state': True, + 'capabilities': list([ + 'AUDIO_PLAYER', + 'MICROPHONE', + ]), + 'device_cluster_members': list([ + 'echo_test_serial_number', + ]), + 'device_family': 'mine', + 'device_locale': 'en-US', + 'device_owner_customer_id': 'amazon_ower_id', + 'device_type': 'echo', + 'do_not_disturb': False, + 'entity_id': '11111111-2222-3333-4444-555555555555', + 'online': True, + 'response_style': None, + 'sensors': dict({ + 'temperature': dict({ + 'name': 'temperature', + 'scale': 'CELSIUS', + 'value': '22.5', + }), + }), + 'serial_number': 'echo_test_serial_number', + 'software_version': 'echo_test_software_version', + }), + 'Play B.B.C. radio on TuneIn', + ), + dict({ + }), + ) +# --- diff --git a/tests/components/alexa_devices/test_services.py b/tests/components/alexa_devices/test_services.py new file mode 100644 index 00000000000..914664199c2 --- /dev/null +++ b/tests/components/alexa_devices/test_services.py @@ -0,0 +1,195 @@ +"""Tests for Alexa Devices services.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.alexa_devices.const import DOMAIN +from homeassistant.components.alexa_devices.services import ( + ATTR_SOUND, + ATTR_SOUND_VARIANT, + ATTR_TEXT_COMMAND, + SERVICE_SOUND_NOTIFICATION, + SERVICE_TEXT_COMMAND, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import device_registry as dr + +from . import setup_integration +from .const import TEST_DEVICE_ID, TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry, mock_device_registry + + +async def test_setup_services( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setup of Alexa Devices services.""" + await setup_integration(hass, mock_config_entry) + + assert (services := hass.services.async_services_for_domain(DOMAIN)) + assert SERVICE_TEXT_COMMAND in services + assert SERVICE_SOUND_NOTIFICATION in services + + +async def test_send_sound_service( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test send sound service.""" + + await setup_integration(hass, mock_config_entry) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device_entry + + await hass.services.async_call( + DOMAIN, + SERVICE_SOUND_NOTIFICATION, + { + ATTR_SOUND: "chimes_bells", + ATTR_SOUND_VARIANT: 1, + ATTR_DEVICE_ID: device_entry.id, + }, + blocking=True, + ) + + assert mock_amazon_devices_client.call_alexa_sound.call_count == 1 + assert mock_amazon_devices_client.call_alexa_sound.call_args == snapshot + + +async def test_send_text_service( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test send text service.""" + + await setup_integration(hass, mock_config_entry) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device_entry + + await hass.services.async_call( + DOMAIN, + SERVICE_TEXT_COMMAND, + { + ATTR_TEXT_COMMAND: "Play B.B.C. radio on TuneIn", + ATTR_DEVICE_ID: device_entry.id, + }, + blocking=True, + ) + + assert mock_amazon_devices_client.call_alexa_text_command.call_count == 1 + assert mock_amazon_devices_client.call_alexa_text_command.call_args == snapshot + + +@pytest.mark.parametrize( + ("sound", "device_id", "translation_key", "translation_placeholders"), + [ + ( + "chimes_bells", + "fake_device_id", + "invalid_device_id", + {"device_id": "fake_device_id"}, + ), + ( + "wrong_sound_name", + TEST_DEVICE_ID, + "invalid_sound_value", + { + "sound": "wrong_sound_name", + "variant": "1", + }, + ), + ], +) +async def test_invalid_parameters( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + sound: str, + device_id: str, + translation_key: str, + translation_placeholders: dict[str, str], +) -> None: + """Test invalid service parameters.""" + + device_entry = dr.DeviceEntry( + id=TEST_DEVICE_ID, identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + mock_device_registry( + hass, + {device_entry.id: device_entry}, + ) + await setup_integration(hass, mock_config_entry) + + # Call Service + with pytest.raises(ServiceValidationError) as exc_info: + await hass.services.async_call( + DOMAIN, + SERVICE_SOUND_NOTIFICATION, + { + ATTR_SOUND: sound, + ATTR_SOUND_VARIANT: 1, + ATTR_DEVICE_ID: device_id, + }, + blocking=True, + ) + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == translation_key + assert exc_info.value.translation_placeholders == translation_placeholders + + +async def test_config_entry_not_loaded( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry not loaded.""" + + await setup_integration(hass, mock_config_entry) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device_entry + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + # Call Service + with pytest.raises(ServiceValidationError) as exc_info: + await hass.services.async_call( + DOMAIN, + SERVICE_SOUND_NOTIFICATION, + { + ATTR_SOUND: "chimes_bells", + ATTR_SOUND_VARIANT: 1, + ATTR_DEVICE_ID: device_entry.id, + }, + blocking=True, + ) + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == "entry_not_loaded" + assert exc_info.value.translation_placeholders == {"entry": mock_config_entry.title} From 69e3a5bc34aee464341d5a700829a422fc690578 Mon Sep 17 00:00:00 2001 From: Samuel Xiao <40679757+XiaoLing-git@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:02:37 +0800 Subject: [PATCH 119/265] Add support for more switchbot cloud vacuum models (#146637) --- .../components/switchbot_cloud/__init__.py | 5 +- .../components/switchbot_cloud/vacuum.py | 140 ++++- .../components/switchbot_cloud/test_vacuum.py | 522 ++++++++++++++++++ 3 files changed, 662 insertions(+), 5 deletions(-) create mode 100644 tests/components/switchbot_cloud/test_vacuum.py diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 482c5c4a9e6..fef156e40db 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -142,12 +142,15 @@ async def make_device_data( hass, entry, api, device, coordinators_by_id ) devices_data.sensors.append((device, coordinator)) - if isinstance(device, Device) and device.device_type in [ "K10+", "K10+ Pro", "Robot Vacuum Cleaner S1", "Robot Vacuum Cleaner S1 Plus", + "K20+ Pro", + "Robot Vacuum Cleaner K10+ Pro Combo", + "Robot Vacuum Cleaner S10", + "S20", ]: coordinator = await coordinator_for_device( hass, entry, api, device, coordinators_by_id, True diff --git a/homeassistant/components/switchbot_cloud/vacuum.py b/homeassistant/components/switchbot_cloud/vacuum.py index 9a9ad49626f..7bc4c7d0ea2 100644 --- a/homeassistant/components/switchbot_cloud/vacuum.py +++ b/homeassistant/components/switchbot_cloud/vacuum.py @@ -2,7 +2,15 @@ from typing import Any -from switchbot_api import Device, Remote, SwitchBotAPI, VacuumCommands +from switchbot_api import ( + Device, + Remote, + SwitchBotAPI, + VacuumCleanerV2Commands, + VacuumCleanerV3Commands, + VacuumCleanMode, + VacuumCommands, +) from homeassistant.components.vacuum import ( StateVacuumEntity, @@ -63,6 +71,11 @@ VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: dict[str, str] = { class SwitchBotCloudVacuum(SwitchBotCloudEntity, StateVacuumEntity): """Representation of a SwitchBot vacuum.""" + # "K10+" + # "K10+ Pro" + # "Robot Vacuum Cleaner S1" + # "Robot Vacuum Cleaner S1 Plus" + _attr_supported_features: VacuumEntityFeature = ( VacuumEntityFeature.BATTERY | VacuumEntityFeature.FAN_SPEED @@ -85,23 +98,26 @@ class SwitchBotCloudVacuum(SwitchBotCloudEntity, StateVacuumEntity): VacuumCommands.POW_LEVEL, parameters=VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED[fan_speed], ) - self.async_write_ha_state() + await self.coordinator.async_request_refresh() async def async_pause(self) -> None: """Pause the cleaning task.""" await self.send_api_command(VacuumCommands.STOP) + self.async_write_ha_state() async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" await self.send_api_command(VacuumCommands.DOCK) + await self.coordinator.async_request_refresh() async def async_start(self) -> None: """Start or resume the cleaning task.""" await self.send_api_command(VacuumCommands.START) + await self.coordinator.async_request_refresh() def _set_attributes(self) -> None: """Set attributes from coordinator data.""" - if not self.coordinator.data: + if self.coordinator.data is None: return self._attr_battery_level = self.coordinator.data.get("battery") @@ -109,11 +125,127 @@ class SwitchBotCloudVacuum(SwitchBotCloudEntity, StateVacuumEntity): switchbot_state = str(self.coordinator.data.get("workingStatus")) self._attr_activity = VACUUM_SWITCHBOT_STATE_TO_HA_STATE.get(switchbot_state) + if self._attr_fan_speed is None: + self._attr_fan_speed = VACUUM_FAN_SPEED_QUIET + + +class SwitchBotCloudVacuumK20PlusPro(SwitchBotCloudVacuum): + """Representation of a SwitchBot K20+ Pro.""" + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + self._attr_fan_speed = fan_speed + await self.send_api_command( + VacuumCleanerV2Commands.CHANGE_PARAM, + parameters={ + "fanLevel": int(VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED[fan_speed]) + 1, + "waterLevel": 1, + "times": 1, + }, + ) + await self.coordinator.async_request_refresh() + + async def async_pause(self) -> None: + """Pause the cleaning task.""" + await self.send_api_command(VacuumCleanerV2Commands.PAUSE) + await self.coordinator.async_request_refresh() + + async def async_return_to_base(self, **kwargs: Any) -> None: + """Set the vacuum cleaner to return to the dock.""" + await self.send_api_command(VacuumCleanerV2Commands.DOCK) + await self.coordinator.async_request_refresh() + + async def async_start(self) -> None: + """Start or resume the cleaning task.""" + fan_level = ( + VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED.get(self.fan_speed) + if self.fan_speed + else None + ) + await self.send_api_command( + VacuumCleanerV2Commands.START_CLEAN, + parameters={ + "action": VacuumCleanMode.SWEEP.value, + "param": { + "fanLevel": int(fan_level if fan_level else VACUUM_FAN_SPEED_QUIET) + + 1, + "times": 1, + }, + }, + ) + await self.coordinator.async_request_refresh() + + +class SwitchBotCloudVacuumK10PlusProCombo(SwitchBotCloudVacuumK20PlusPro): + """Representation of a SwitchBot vacuum K10+ Pro Combo.""" + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + self._attr_fan_speed = fan_speed + if fan_speed in VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: + await self.send_api_command( + VacuumCleanerV2Commands.CHANGE_PARAM, + parameters={ + "fanLevel": int(VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED[fan_speed]) + + 1, + "times": 1, + }, + ) + await self.coordinator.async_request_refresh() + + +class SwitchBotCloudVacuumV3(SwitchBotCloudVacuumK20PlusPro): + """Representation of a SwitchBot vacuum Robot Vacuum Cleaner S10 & S20.""" + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + self._attr_fan_speed = fan_speed + await self.send_api_command( + VacuumCleanerV3Commands.CHANGE_PARAM, + parameters={ + "fanLevel": int(VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED[fan_speed]) + 1, + "waterLevel": 1, + "times": 1, + }, + ) + await self.coordinator.async_request_refresh() + + async def async_start(self) -> None: + """Start or resume the cleaning task.""" + fan_level = ( + VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED.get(self.fan_speed) + if self.fan_speed + else None + ) + await self.send_api_command( + VacuumCleanerV3Commands.START_CLEAN, + parameters={ + "action": VacuumCleanMode.SWEEP.value, + "param": { + "fanLevel": int(fan_level if fan_level else VACUUM_FAN_SPEED_QUIET), + "waterLevel": 1, + "times": 1, + }, + }, + ) + await self.coordinator.async_request_refresh() @callback def _async_make_entity( api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator -) -> SwitchBotCloudVacuum: +) -> ( + SwitchBotCloudVacuum + | SwitchBotCloudVacuumK20PlusPro + | SwitchBotCloudVacuumV3 + | SwitchBotCloudVacuumK10PlusProCombo +): """Make a SwitchBotCloudVacuum.""" + if device.device_type in VacuumCleanerV2Commands.get_supported_devices(): + if device.device_type == "K20+ Pro": + return SwitchBotCloudVacuumK20PlusPro(api, device, coordinator) + return SwitchBotCloudVacuumK10PlusProCombo(api, device, coordinator) + + if device.device_type in VacuumCleanerV3Commands.get_supported_devices(): + return SwitchBotCloudVacuumV3(api, device, coordinator) return SwitchBotCloudVacuum(api, device, coordinator) diff --git a/tests/components/switchbot_cloud/test_vacuum.py b/tests/components/switchbot_cloud/test_vacuum.py new file mode 100644 index 00000000000..daa52f4f183 --- /dev/null +++ b/tests/components/switchbot_cloud/test_vacuum.py @@ -0,0 +1,522 @@ +"""Test for the switchbot_cloud vacuum.""" + +from unittest.mock import patch + +from switchbot_api import ( + Device, + VacuumCleanerV2Commands, + VacuumCleanerV3Commands, + VacuumCleanMode, + VacuumCommands, +) + +from homeassistant.components.switchbot_cloud import SwitchBotAPI +from homeassistant.components.switchbot_cloud.const import VACUUM_FAN_SPEED_QUIET +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, + DOMAIN as VACUUM_DOMAIN, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + VacuumActivity, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +async def test_coordinator_data_is_none( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test coordinator data is none.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + None, + ] + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == STATE_UNKNOWN + + +async def test_k10_plus_set_fan_speed( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K10 plus set fan speed.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K10+", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_SPEED: VACUUM_FAN_SPEED_QUIET}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCommands.POW_LEVEL, "command", "0" + ) + + +async def test_k10_plus_return_to_base( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test k10 plus return to base.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + { + "deviceType": "K10+", + "workingStatus": "Charging", + "battery": 50, + "onlineStatus": "online", + } + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == VacuumActivity.DOCKED.value + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_RETURN_TO_BASE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCommands.DOCK, "command", "default" + ) + + +async def test_k10_plus_pause( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test k10 plus pause.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + { + "deviceType": "K10+", + "workingStatus": "Charging", + "battery": 50, + "onlineStatus": "online", + } + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == VacuumActivity.DOCKED.value + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, SERVICE_PAUSE, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCommands.STOP, "command", "default" + ) + + +async def test_k10_plus_set_start( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K10 plus start.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K10+", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_START, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCommands.START, "command", "default" + ) + + +async def test_k20_plus_pro_set_fan_speed( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K10 plus set fan speed.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K20+ Pro", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K20+ Pro", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_SPEED: VACUUM_FAN_SPEED_QUIET}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV2Commands.CHANGE_PARAM, + "command", + { + "fanLevel": 1, + "waterLevel": 1, + "times": 1, + }, + ) + + +async def test_k20_plus_pro_return_to_base( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K20+ Pro return to base.""" + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K20+ Pro", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K20+ Pro", + "workingStatus": "Charging", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == VacuumActivity.DOCKED.value + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_RETURN_TO_BASE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCleanerV2Commands.DOCK, "command", "default" + ) + + +async def test_k20_plus_pro_pause( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K20+ Pro pause.""" + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K20+ Pro", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K20+ Pro", + "workingStatus": "Charging", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == VacuumActivity.DOCKED.value + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, SERVICE_PAUSE, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCleanerV2Commands.PAUSE, "command", "default" + ) + + +async def test_k20_plus_pro_start( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K20+ Pro start.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K20+ Pro", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K20+ Pro", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_START, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV2Commands.START_CLEAN, + "command", + { + "action": VacuumCleanMode.SWEEP.value, + "param": { + "fanLevel": 1, + "times": 1, + }, + }, + ) + + +async def test_k10_plus_pro_combo_set_fan_speed( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test k10+ Pro Combo set fan speed.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="Robot Vacuum Cleaner K10+ Pro Combo", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "Robot Vacuum Cleaner K10+ Pro Combo", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_SPEED: VACUUM_FAN_SPEED_QUIET}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV2Commands.CHANGE_PARAM, + "command", + { + "fanLevel": 1, + "times": 1, + }, + ) + + +async def test_s20_start( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test s20 start.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="S20", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "s20", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_START, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV3Commands.START_CLEAN, + "command", + { + "action": VacuumCleanMode.SWEEP.value, + "param": { + "fanLevel": 0, + "waterLevel": 1, + "times": 1, + }, + }, + ) + + +async def test_s20set_fan_speed( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test s20 set fan speed.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="S20", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "S20", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_SPEED: VACUUM_FAN_SPEED_QUIET}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV3Commands.CHANGE_PARAM, + "command", + { + "fanLevel": 1, + "waterLevel": 1, + "times": 1, + }, + ) From 260ca707852433057b5743d1a91edb72eebf8ad0 Mon Sep 17 00:00:00 2001 From: Samuel Xiao <40679757+XiaoLing-git@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:03:13 +0800 Subject: [PATCH 120/265] Add Light platform to Switchbot cloud (#146382) --- .../components/switchbot_cloud/__init__.py | 13 + .../components/switchbot_cloud/const.py | 2 + .../components/switchbot_cloud/light.py | 153 +++++++++ tests/components/switchbot_cloud/conftest.py | 9 + .../components/switchbot_cloud/test_light.py | 300 ++++++++++++++++++ 5 files changed, 477 insertions(+) create mode 100644 homeassistant/components/switchbot_cloud/light.py create mode 100644 tests/components/switchbot_cloud/test_light.py diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index fef156e40db..ae3a32997ae 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -30,6 +30,7 @@ PLATFORMS: list[Platform] = [ Platform.BUTTON, Platform.CLIMATE, Platform.FAN, + Platform.LIGHT, Platform.LOCK, Platform.SENSOR, Platform.SWITCH, @@ -53,6 +54,7 @@ class SwitchbotDevices: vacuums: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) fans: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) + lights: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) @dataclass @@ -191,6 +193,17 @@ async def make_device_data( devices_data.fans.append((device, coordinator)) devices_data.sensors.append((device, coordinator)) + if isinstance(device, Device) and device.device_type in [ + "Strip Light", + "Strip Light 3", + "Floor Lamp", + "Color Bulb", + ]: + coordinator = await coordinator_for_device( + hass, entry, api, device, coordinators_by_id + ) + devices_data.lights.append((device, coordinator)) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SwitchBot via API from a config entry.""" diff --git a/homeassistant/components/switchbot_cloud/const.py b/homeassistant/components/switchbot_cloud/const.py index b849194537a..dcca5119a74 100644 --- a/homeassistant/components/switchbot_cloud/const.py +++ b/homeassistant/components/switchbot_cloud/const.py @@ -15,3 +15,5 @@ VACUUM_FAN_SPEED_QUIET = "quiet" VACUUM_FAN_SPEED_STANDARD = "standard" VACUUM_FAN_SPEED_STRONG = "strong" VACUUM_FAN_SPEED_MAX = "max" + +AFTER_COMMAND_REFRESH = 5 diff --git a/homeassistant/components/switchbot_cloud/light.py b/homeassistant/components/switchbot_cloud/light.py new file mode 100644 index 00000000000..645c6b4c62b --- /dev/null +++ b/homeassistant/components/switchbot_cloud/light.py @@ -0,0 +1,153 @@ +"""Support for the Switchbot Light.""" + +import asyncio +from typing import Any + +from switchbot_api import ( + CommonCommands, + Device, + Remote, + RGBWLightCommands, + RGBWWLightCommands, + SwitchBotAPI, +) + +from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import SwitchbotCloudData, SwitchBotCoordinator +from .const import AFTER_COMMAND_REFRESH, DOMAIN +from .entity import SwitchBotCloudEntity + + +def value_map_brightness(value: int) -> int: + """Return value for brightness map.""" + return int(value / 255 * 100) + + +async def async_setup_entry( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up SwitchBot Cloud entry.""" + data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] + async_add_entities( + _async_make_entity(data.api, device, coordinator) + for device, coordinator in data.devices.lights + ) + + +class SwitchBotCloudLight(SwitchBotCloudEntity, LightEntity): + """Base Class for SwitchBot Light.""" + + _attr_is_on: bool | None = None + _attr_name: str | None = None + + _attr_color_mode = ColorMode.UNKNOWN + + def _set_attributes(self) -> None: + """Set attributes from coordinator data.""" + if self.coordinator.data is None: + return + + power: str | None = self.coordinator.data.get("power") + brightness: int | None = self.coordinator.data.get("brightness") + color: str | None = self.coordinator.data.get("color") + color_temperature: int | None = self.coordinator.data.get("colorTemperature") + self._attr_is_on = power == "on" if power else None + self._attr_brightness: int | None = brightness if brightness else None + self._attr_rgb_color: tuple | None = ( + (tuple(int(i) for i in color.split(":"))) if color else None + ) + self._attr_color_temp_kelvin: int | None = ( + color_temperature if color_temperature else None + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.send_api_command(CommonCommands.OFF) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + brightness: int | None = kwargs.get("brightness") + rgb_color: tuple[int, int, int] | None = kwargs.get("rgb_color") + color_temp_kelvin: int | None = kwargs.get("color_temp_kelvin") + if brightness is not None: + self._attr_color_mode = ColorMode.RGB + await self._send_brightness_command(brightness) + elif rgb_color is not None: + self._attr_color_mode = ColorMode.RGB + await self._send_rgb_color_command(rgb_color) + elif color_temp_kelvin is not None: + self._attr_color_mode = ColorMode.COLOR_TEMP + await self._send_color_temperature_command(color_temp_kelvin) + else: + self._attr_color_mode = ColorMode.RGB + await self.send_api_command(CommonCommands.ON) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def _send_brightness_command(self, brightness: int) -> None: + """Send a brightness command.""" + await self.send_api_command( + RGBWLightCommands.SET_BRIGHTNESS, + parameters=str(value_map_brightness(brightness)), + ) + + async def _send_rgb_color_command(self, rgb_color: tuple) -> None: + """Send an RGB command.""" + await self.send_api_command( + RGBWLightCommands.SET_COLOR, + parameters=f"{rgb_color[2]}:{rgb_color[1]}:{rgb_color[0]}", + ) + + async def _send_color_temperature_command(self, color_temp_kelvin: int) -> None: + """Send a color temperature command.""" + await self.send_api_command( + RGBWWLightCommands.SET_COLOR_TEMPERATURE, + parameters=str(color_temp_kelvin), + ) + + +class SwitchBotCloudStripLight(SwitchBotCloudLight): + """Representation of a SwitchBot Strip Light.""" + + _attr_supported_color_modes = {ColorMode.RGB} + + +class SwitchBotCloudRGBWWLight(SwitchBotCloudLight): + """Representation of SwitchBot |Strip Light|Floor Lamp|Color Bulb.""" + + _attr_max_color_temp_kelvin = 6500 + _attr_min_color_temp_kelvin = 2700 + + _attr_supported_color_modes = {ColorMode.RGB, ColorMode.COLOR_TEMP} + + async def _send_brightness_command(self, brightness: int) -> None: + """Send a brightness command.""" + await self.send_api_command( + RGBWWLightCommands.SET_BRIGHTNESS, + parameters=str(value_map_brightness(brightness)), + ) + + async def _send_rgb_color_command(self, rgb_color: tuple) -> None: + """Send an RGB command.""" + await self.send_api_command( + RGBWWLightCommands.SET_COLOR, + parameters=f"{rgb_color[0]}:{rgb_color[1]}:{rgb_color[2]}", + ) + + +@callback +def _async_make_entity( + api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator +) -> SwitchBotCloudStripLight | SwitchBotCloudRGBWWLight: + """Make a SwitchBotCloudLight.""" + if device.device_type == "Strip Light": + return SwitchBotCloudStripLight(api, device, coordinator) + return SwitchBotCloudRGBWWLight(api, device, coordinator) diff --git a/tests/components/switchbot_cloud/conftest.py b/tests/components/switchbot_cloud/conftest.py index 09c953da06b..27214fde28d 100644 --- a/tests/components/switchbot_cloud/conftest.py +++ b/tests/components/switchbot_cloud/conftest.py @@ -30,3 +30,12 @@ def mock_get_status(): """Mock get_status.""" with patch.object(SwitchBotAPI, "get_status") as mock_get_status: yield mock_get_status + + +@pytest.fixture(scope="package", autouse=True) +def mock_after_command_refresh(): + """Mock after command refresh.""" + with patch( + "homeassistant.components.switchbot_cloud.const.AFTER_COMMAND_REFRESH", 0 + ): + yield diff --git a/tests/components/switchbot_cloud/test_light.py b/tests/components/switchbot_cloud/test_light.py new file mode 100644 index 00000000000..e4f39c0d530 --- /dev/null +++ b/tests/components/switchbot_cloud/test_light.py @@ -0,0 +1,300 @@ +"""Test for the Switchbot Light Entity.""" + +from unittest.mock import patch + +from switchbot_api import Device, SwitchBotAPI + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +async def test_coordinator_data_is_none( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test coordinator data is none.""" + + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [None] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + state = hass.states.get(entity_id) + assert state.state is STATE_UNKNOWN + + +async def test_strip_light_turn_off( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test strip light turn off.""" + + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "color": "0:0:0", "colorTemperature": 4567}, + {"power": "off", "brightness": 10, "color": "0:0:0", "colorTemperature": 5555}, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + # state = hass.states.get(entity_id) + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once() + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + + +async def test_rgbww_light_turn_off( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test rgbww light turn_off.""" + + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light 3", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "color": "0:0:0", "colorTemperature": 4567}, + {"power": "off", "brightness": 10, "color": "0:0:0", "colorTemperature": 5555}, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once() + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + + +async def test_strip_light_turn_on( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test strip light turn on.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "color": "0:0:0", "colorTemperature": 4567}, + {"power": "on", "brightness": 10, "color": "0:0:0", "colorTemperature": 5555}, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "brightness": 99}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "rgb_color": (255, 246, 158)}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "color_temp_kelvin": 3333}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + +async def test_rgbww_light_turn_on( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test rgbww light turn on.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light 3", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "color": "0:0:0", "colorTemperature": 4567}, + {"power": "on", "brightness": 10, "color": "0:0:0", "colorTemperature": 5555}, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "color_temp_kelvin": 2800}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "brightness": 99}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "rgb_color": (255, 246, 158)}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON From 25169e9075628556324fd82aef9f38c61ea79cec Mon Sep 17 00:00:00 2001 From: Avery <130164016+avedor@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:06:38 -0400 Subject: [PATCH 121/265] Bump datadogpy to 0.52.0 (#149596) --- homeassistant/components/datadog/__init__.py | 4 +++- .../components/datadog/config_flow.py | 22 +++++++++++++++++-- homeassistant/components/datadog/const.py | 2 +- .../components/datadog/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/datadog/test_init.py | 4 ++-- 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index 606f34c9ae0..219f3afe4e2 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -75,7 +75,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: DatadogConfigEntry) -> b prefix = options[CONF_PREFIX] sample_rate = options[CONF_RATE] - statsd_client = DogStatsd(host=host, port=port, namespace=prefix) + statsd_client = DogStatsd( + host=host, port=port, namespace=prefix, disable_telemetry=True + ) entry.runtime_data = statsd_client initialize(statsd_host=host, statsd_port=port) diff --git a/homeassistant/components/datadog/config_flow.py b/homeassistant/components/datadog/config_flow.py index 876b79b6019..a2ad74e2c57 100644 --- a/homeassistant/components/datadog/config_flow.py +++ b/homeassistant/components/datadog/config_flow.py @@ -58,7 +58,6 @@ class DatadogConfigFlow(ConfigFlow, domain=DOMAIN): CONF_RATE: user_input[CONF_RATE], }, ) - return self.async_show_form( step_id="user", data_schema=vol.Schema( @@ -107,7 +106,26 @@ class DatadogOptionsFlowHandler(OptionsFlow): options = self.config_entry.options if user_input is None: - user_input = {} + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_PREFIX, + default=options.get( + CONF_PREFIX, data.get(CONF_PREFIX, DEFAULT_PREFIX) + ), + ): str, + vol.Required( + CONF_RATE, + default=options.get( + CONF_RATE, data.get(CONF_RATE, DEFAULT_RATE) + ), + ): int, + } + ), + errors={}, + ) success = await validate_datadog_connection( self.hass, diff --git a/homeassistant/components/datadog/const.py b/homeassistant/components/datadog/const.py index e9e5d80eeba..7c9a0311228 100644 --- a/homeassistant/components/datadog/const.py +++ b/homeassistant/components/datadog/const.py @@ -4,7 +4,7 @@ DOMAIN = "datadog" CONF_RATE = "rate" -DEFAULT_HOST = "localhost" +DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 8125 DEFAULT_PREFIX = "hass" DEFAULT_RATE = 1 diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index 815446b9ab4..798a314e307 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["datadog"], "quality_scale": "legacy", - "requirements": ["datadog==0.15.0"] + "requirements": ["datadog==0.52.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 93663598733..8c68449f7d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -759,7 +759,7 @@ crownstone-sse==2.0.5 crownstone-uart==2.1.0 # homeassistant.components.datadog -datadog==0.15.0 +datadog==0.52.0 # homeassistant.components.metoffice datapoint==0.12.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 268d263220a..0f2cf2c491e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -662,7 +662,7 @@ crownstone-sse==2.0.5 crownstone-uart==2.1.0 # homeassistant.components.datadog -datadog==0.15.0 +datadog==0.52.0 # homeassistant.components.metoffice datapoint==0.12.1 diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 3c22aaeee8f..7ab9e0cb97a 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -46,7 +46,7 @@ async def test_datadog_setup_full(hass: HomeAssistant) -> None: assert mock_dogstatsd.call_count == 1 assert mock_dogstatsd.call_args == mock.call( - host="host", port=123, namespace="foo" + host="host", port=123, namespace="foo", disable_telemetry=True ) @@ -65,7 +65,7 @@ async def test_datadog_setup_defaults(hass: HomeAssistant) -> None: assert mock_dogstatsd.call_count == 1 assert mock_dogstatsd.call_args == mock.call( - host="localhost", port=8125, namespace="hass" + host="localhost", port=8125, namespace="hass", disable_telemetry=True ) From d8016f7f41f5b69851b9e2a586dad8c022a7a9bf Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:06:59 +0200 Subject: [PATCH 122/265] Remove stale devices in Uptime Kuma (#149605) --- .../components/uptime_kuma/__init__.py | 24 +++++- tests/components/uptime_kuma/test_init.py | 85 +++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/uptime_kuma/__init__.py b/homeassistant/components/uptime_kuma/__init__.py index 68234077976..4efe6a68193 100644 --- a/homeassistant/components/uptime_kuma/__init__.py +++ b/homeassistant/components/uptime_kuma/__init__.py @@ -6,7 +6,7 @@ from pythonkuma.update import UpdateChecker from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.hass_dict import HassKey @@ -43,6 +43,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) - return True +async def async_remove_config_entry_device( + hass: HomeAssistant, + config_entry: UptimeKumaConfigEntry, + device_entry: dr.DeviceEntry, +) -> bool: + """Remove a stale device from a config entry.""" + + def normalize_key(id: str) -> int | str: + key = id.removeprefix(f"{config_entry.entry_id}_") + return int(key) if key.isnumeric() else key + + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + and ( + identifier[1] == config_entry.entry_id + or normalize_key(identifier[1]) in config_entry.runtime_data.data + ) + ) + + async def async_unload_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, _PLATFORMS) diff --git a/tests/components/uptime_kuma/test_init.py b/tests/components/uptime_kuma/test_init.py index 6e2ef43b14d..61d196f0263 100644 --- a/tests/components/uptime_kuma/test_init.py +++ b/tests/components/uptime_kuma/test_init.py @@ -8,8 +8,11 @@ from pythonkuma import UptimeKumaAuthenticationException, UptimeKumaException from homeassistant.components.uptime_kuma.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator @pytest.mark.usefixtures("mock_pythonkuma") @@ -77,3 +80,85 @@ async def test_config_reauth_flow( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("entry_id") == config_entry.entry_id + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_remove_stale_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test we can remove a device that is not in the coordinator data.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "123456789_1")} + ) + + config_entry.runtime_data.data.pop(1) + response = await ws_client.remove_device(device_entry.id, config_entry.entry_id) + + assert response["success"] + assert ( + device_registry.async_get_device(identifiers={(DOMAIN, "123456789_1")}) is None + ) + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_remove_current_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test we cannot remove a device if it is still active.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "123456789_1")} + ) + + response = await ws_client.remove_device(device_entry.id, config_entry.entry_id) + + assert response["success"] is False + assert device_registry.async_get_device(identifiers={(DOMAIN, "123456789_1")}) + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_remove_entry_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test we cannot remove the device with the update entity.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "123456789")}) + + response = await ws_client.remove_device(device_entry.id, config_entry.entry_id) + + assert response["success"] is False + assert device_registry.async_get_device(identifiers={(DOMAIN, "123456789")}) From 779f0afcc452ae49628fd604fc45199030bb9a77 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:07:22 +0200 Subject: [PATCH 123/265] Refactor Habitica button and switch functions to use habiticalib instance directly (#149602) --- homeassistant/components/habitica/button.py | 103 ++++-------------- .../components/habitica/coordinator.py | 12 +- homeassistant/components/habitica/switch.py | 16 ++- 3 files changed, 37 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/habitica/button.py b/homeassistant/components/habitica/button.py index c57ba39fb6a..de8920deb77 100644 --- a/homeassistant/components/habitica/button.py +++ b/homeassistant/components/habitica/button.py @@ -7,15 +7,7 @@ from dataclasses import dataclass from enum import StrEnum from typing import Any -from aiohttp import ClientError -from habiticalib import ( - HabiticaClass, - HabiticaException, - NotAuthorizedError, - Skill, - TaskType, - TooManyRequestsError, -) +from habiticalib import Habitica, HabiticaClass, Skill, TaskType from homeassistant.components.button import ( DOMAIN as BUTTON_DOMAIN, @@ -23,16 +15,11 @@ from homeassistant.components.button import ( ButtonEntityDescription, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ASSETS_URL, DOMAIN -from .coordinator import ( - HabiticaConfigEntry, - HabiticaData, - HabiticaDataUpdateCoordinator, -) +from .coordinator import HabiticaConfigEntry, HabiticaData from .entity import HabiticaBase PARALLEL_UPDATES = 1 @@ -42,7 +29,7 @@ PARALLEL_UPDATES = 1 class HabiticaButtonEntityDescription(ButtonEntityDescription): """Describes Habitica button entity.""" - press_fn: Callable[[HabiticaDataUpdateCoordinator], Any] + press_fn: Callable[[Habitica], Any] available_fn: Callable[[HabiticaData], bool] class_needed: HabiticaClass | None = None entity_picture: str | None = None @@ -73,13 +60,13 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.RUN_CRON, translation_key=HabiticaButtonEntity.RUN_CRON, - press_fn=lambda coordinator: coordinator.habitica.run_cron(), + press_fn=lambda habitica: habitica.run_cron(), available_fn=lambda data: data.user.needsCron is True, ), HabiticaButtonEntityDescription( key=HabiticaButtonEntity.BUY_HEALTH_POTION, translation_key=HabiticaButtonEntity.BUY_HEALTH_POTION, - press_fn=lambda coordinator: coordinator.habitica.buy_health_potion(), + press_fn=lambda habitica: habitica.buy_health_potion(), available_fn=( lambda data: (data.user.stats.gp or 0) >= 25 and (data.user.stats.hp or 0) < 50 @@ -89,7 +76,7 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS, translation_key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS, - press_fn=lambda coordinator: coordinator.habitica.allocate_stat_points(), + press_fn=lambda habitica: habitica.allocate_stat_points(), available_fn=( lambda data: data.user.preferences.automaticAllocation is True and (data.user.stats.points or 0) > 0 @@ -98,7 +85,7 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.REVIVE, translation_key=HabiticaButtonEntity.REVIVE, - press_fn=lambda coordinator: coordinator.habitica.revive(), + press_fn=lambda habitica: habitica.revive(), available_fn=lambda data: data.user.stats.hp == 0, ), ) @@ -108,9 +95,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.MPHEAL, translation_key=HabiticaButtonEntity.MPHEAL, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.ETHEREAL_SURGE) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.ETHEREAL_SURGE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 12 and (data.user.stats.mp or 0) >= 30 @@ -121,7 +106,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.EARTH, translation_key=HabiticaButtonEntity.EARTH, - press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.EARTHQUAKE), + press_fn=lambda habitica: habitica.cast_skill(Skill.EARTHQUAKE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 13 and (data.user.stats.mp or 0) >= 35 @@ -132,9 +117,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.FROST, translation_key=HabiticaButtonEntity.FROST, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.CHILLING_FROST) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.CHILLING_FROST), # chilling frost can only be cast once per day (streaks buff is false) available_fn=( lambda data: (data.user.stats.lvl or 0) >= 14 @@ -147,9 +130,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.DEFENSIVE_STANCE, translation_key=HabiticaButtonEntity.DEFENSIVE_STANCE, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.DEFENSIVE_STANCE) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.DEFENSIVE_STANCE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 12 and (data.user.stats.mp or 0) >= 25 @@ -160,9 +141,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.VALOROUS_PRESENCE, translation_key=HabiticaButtonEntity.VALOROUS_PRESENCE, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.VALOROUS_PRESENCE) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.VALOROUS_PRESENCE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 13 and (data.user.stats.mp or 0) >= 20 @@ -173,9 +152,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.INTIMIDATE, translation_key=HabiticaButtonEntity.INTIMIDATE, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.INTIMIDATING_GAZE) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.INTIMIDATING_GAZE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 14 and (data.user.stats.mp or 0) >= 15 @@ -186,11 +163,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.TOOLS_OF_TRADE, translation_key=HabiticaButtonEntity.TOOLS_OF_TRADE, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill( - Skill.TOOLS_OF_THE_TRADE - ) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.TOOLS_OF_THE_TRADE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 13 and (data.user.stats.mp or 0) >= 25 @@ -201,7 +174,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.STEALTH, translation_key=HabiticaButtonEntity.STEALTH, - press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.STEALTH), + press_fn=lambda habitica: habitica.cast_skill(Skill.STEALTH), # Stealth buffs stack and it can only be cast if the amount of # buffs is smaller than the amount of unfinished dailies available_fn=( @@ -224,9 +197,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.HEAL, translation_key=HabiticaButtonEntity.HEAL, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.HEALING_LIGHT) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.HEALING_LIGHT), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 11 and (data.user.stats.mp or 0) >= 15 @@ -238,11 +209,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.BRIGHTNESS, translation_key=HabiticaButtonEntity.BRIGHTNESS, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill( - Skill.SEARING_BRIGHTNESS - ) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.SEARING_BRIGHTNESS), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 12 and (data.user.stats.mp or 0) >= 15 @@ -253,9 +220,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.PROTECT_AURA, translation_key=HabiticaButtonEntity.PROTECT_AURA, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.PROTECTIVE_AURA) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.PROTECTIVE_AURA), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 13 and (data.user.stats.mp or 0) >= 30 @@ -266,7 +231,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.HEAL_ALL, translation_key=HabiticaButtonEntity.HEAL_ALL, - press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.BLESSING), + press_fn=lambda habitica: habitica.cast_skill(Skill.BLESSING), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 14 and (data.user.stats.mp or 0) >= 25 @@ -332,33 +297,9 @@ class HabiticaButton(HabiticaBase, ButtonEntity): async def async_press(self) -> None: """Handle the button press.""" - try: - await self.entity_description.press_fn(self.coordinator) - except TooManyRequestsError as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="setup_rate_limit_exception", - translation_placeholders={"retry_after": str(e.retry_after)}, - ) from e - except NotAuthorizedError as e: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="service_call_unallowed", - ) from e - except HabiticaException as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="service_call_exception", - translation_placeholders={"reason": e.error.message}, - ) from e - except ClientError as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="service_call_exception", - translation_placeholders={"reason": str(e)}, - ) from e - else: - await self.coordinator.async_request_refresh() + + await self.coordinator.execute(self.entity_description.press_fn) + await self.coordinator.async_request_refresh() @property def available(self) -> bool: diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index b25edc7ceaf..0e0a2db8d58 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -28,6 +28,7 @@ from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError, + ServiceValidationError, ) from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -130,19 +131,22 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): else: return HabiticaData(user=user, tasks=tasks + completed_todos) - async def execute( - self, func: Callable[[HabiticaDataUpdateCoordinator], Any] - ) -> None: + async def execute(self, func: Callable[[Habitica], Any]) -> None: """Execute an API call.""" try: - await func(self) + await func(self.habitica) except TooManyRequestsError as e: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="setup_rate_limit_exception", translation_placeholders={"retry_after": str(e.retry_after)}, ) from e + except NotAuthorizedError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_unallowed", + ) from e except HabiticaException as e: raise HomeAssistantError( translation_domain=DOMAIN, diff --git a/homeassistant/components/habitica/switch.py b/homeassistant/components/habitica/switch.py index fb98460f7e5..826cd341bba 100644 --- a/homeassistant/components/habitica/switch.py +++ b/homeassistant/components/habitica/switch.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from enum import StrEnum from typing import Any +from habiticalib import Habitica + from homeassistant.components.switch import ( SwitchDeviceClass, SwitchEntity, @@ -15,11 +17,7 @@ from homeassistant.components.switch import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .coordinator import ( - HabiticaConfigEntry, - HabiticaData, - HabiticaDataUpdateCoordinator, -) +from .coordinator import HabiticaConfigEntry, HabiticaData from .entity import HabiticaBase PARALLEL_UPDATES = 1 @@ -29,8 +27,8 @@ PARALLEL_UPDATES = 1 class HabiticaSwitchEntityDescription(SwitchEntityDescription): """Describes Habitica switch entity.""" - turn_on_fn: Callable[[HabiticaDataUpdateCoordinator], Any] - turn_off_fn: Callable[[HabiticaDataUpdateCoordinator], Any] + turn_on_fn: Callable[[Habitica], Any] + turn_off_fn: Callable[[Habitica], Any] is_on_fn: Callable[[HabiticaData], bool | None] @@ -45,8 +43,8 @@ SWTICH_DESCRIPTIONS: tuple[HabiticaSwitchEntityDescription, ...] = ( key=HabiticaSwitchEntity.SLEEP, translation_key=HabiticaSwitchEntity.SLEEP, device_class=SwitchDeviceClass.SWITCH, - turn_on_fn=lambda coordinator: coordinator.habitica.toggle_sleep(), - turn_off_fn=lambda coordinator: coordinator.habitica.toggle_sleep(), + turn_on_fn=lambda habitica: habitica.toggle_sleep(), + turn_off_fn=lambda habitica: habitica.toggle_sleep(), is_on_fn=lambda data: data.user.preferences.sleep, ), ) From dd0b23afb06b939315587fdde63739d0b622bdc2 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Wed, 30 Jul 2025 23:07:47 +1000 Subject: [PATCH 124/265] husqvarna_automower_ble: Support battery percentage sensor (#146159) Signed-off-by: Alistair Francis --- .../husqvarna_automower_ble/__init__.py | 1 + .../husqvarna_automower_ble/coordinator.py | 6 +-- .../husqvarna_automower_ble/entity.py | 16 ++++++ .../husqvarna_automower_ble/sensor.py | 51 ++++++++++++++++++ .../snapshots/test_sensor.ambr | 54 +++++++++++++++++++ .../husqvarna_automower_ble/test_sensor.py | 32 +++++++++++ 6 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/husqvarna_automower_ble/sensor.py create mode 100644 tests/components/husqvarna_automower_ble/snapshots/test_sensor.ambr create mode 100644 tests/components/husqvarna_automower_ble/test_sensor.py diff --git a/homeassistant/components/husqvarna_automower_ble/__init__.py b/homeassistant/components/husqvarna_automower_ble/__init__.py index f168e84be4c..fd4521549a2 100644 --- a/homeassistant/components/husqvarna_automower_ble/__init__.py +++ b/homeassistant/components/husqvarna_automower_ble/__init__.py @@ -19,6 +19,7 @@ type HusqvarnaConfigEntry = ConfigEntry[HusqvarnaCoordinator] PLATFORMS = [ Platform.LAWN_MOWER, + Platform.SENSOR, ] diff --git a/homeassistant/components/husqvarna_automower_ble/coordinator.py b/homeassistant/components/husqvarna_automower_ble/coordinator.py index c7781becd76..ef9ccfa5a47 100644 --- a/homeassistant/components/husqvarna_automower_ble/coordinator.py +++ b/homeassistant/components/husqvarna_automower_ble/coordinator.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: SCAN_INTERVAL = timedelta(seconds=60) -class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, bytes]]): +class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, str | int]]): """Class to manage fetching data.""" def __init__( @@ -67,11 +67,11 @@ class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, bytes]]): except BleakError as err: raise UpdateFailed("Failed to connect") from err - async def _async_update_data(self) -> dict[str, bytes]: + async def _async_update_data(self) -> dict[str, str | int]: """Poll the device.""" LOGGER.debug("Polling device") - data: dict[str, bytes] = {} + data: dict[str, str | int] = {} try: if not self.mower.is_connected(): diff --git a/homeassistant/components/husqvarna_automower_ble/entity.py b/homeassistant/components/husqvarna_automower_ble/entity.py index d2873d933ff..cb62f36027a 100644 --- a/homeassistant/components/husqvarna_automower_ble/entity.py +++ b/homeassistant/components/husqvarna_automower_ble/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER @@ -28,3 +29,18 @@ class HusqvarnaAutomowerBleEntity(CoordinatorEntity[HusqvarnaCoordinator]): def available(self) -> bool: """Return if entity is available.""" return super().available and self.coordinator.mower.is_connected() + + +class HusqvarnaAutomowerBleDescriptorEntity(HusqvarnaAutomowerBleEntity): + """Coordinator entity for entities with entity description.""" + + def __init__( + self, coordinator: HusqvarnaCoordinator, description: EntityDescription + ) -> None: + """Initialize description entity.""" + super().__init__(coordinator) + + self._attr_unique_id = ( + f"{coordinator.address}_{coordinator.channel_id}_{description.key}" + ) + self.entity_description = description diff --git a/homeassistant/components/husqvarna_automower_ble/sensor.py b/homeassistant/components/husqvarna_automower_ble/sensor.py new file mode 100644 index 00000000000..f747133c950 --- /dev/null +++ b/homeassistant/components/husqvarna_automower_ble/sensor.py @@ -0,0 +1,51 @@ +"""Support for sensor entities.""" + +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import HusqvarnaConfigEntry +from .entity import HusqvarnaAutomowerBleDescriptorEntity + +DESCRIPTIONS = ( + SensorEntityDescription( + key="battery_level", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: HusqvarnaConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Husqvarna Automower Ble sensor based on a config entry.""" + coordinator = entry.runtime_data + async_add_entities( + HusqvarnaAutomowerBleSensor(coordinator, description) + for description in DESCRIPTIONS + if description.key in coordinator.data + ) + + +class HusqvarnaAutomowerBleSensor(HusqvarnaAutomowerBleDescriptorEntity, SensorEntity): + """Representation of a sensor.""" + + entity_description: SensorEntityDescription + + @property + def native_value(self) -> str | int: + """Return the previously fetched value.""" + return self.coordinator.data[self.entity_description.key] diff --git a/tests/components/husqvarna_automower_ble/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower_ble/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..8f2bfadf56a --- /dev/null +++ b/tests/components/husqvarna_automower_ble/snapshots/test_sensor.ambr @@ -0,0 +1,54 @@ +# serializer version: 1 +# name: test_setup[sensor.husqvarna_automower_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.husqvarna_automower_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'husqvarna_automower_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000-0000-0000-0000-000000000003_1197489078_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[sensor.husqvarna_automower_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Husqvarna AutoMower Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.husqvarna_automower_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- diff --git a/tests/components/husqvarna_automower_ble/test_sensor.py b/tests/components/husqvarna_automower_ble/test_sensor.py new file mode 100644 index 00000000000..d1f0a13cc43 --- /dev/null +++ b/tests/components/husqvarna_automower_ble/test_sensor.py @@ -0,0 +1,32 @@ +"""Test the Husqvarna Automower Bluetooth setup.""" + +from unittest.mock import patch + +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 tests.common import MockConfigEntry, snapshot_platform + +pytestmark = pytest.mark.usefixtures("mock_automower_client") + + +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test setup creates expected entities.""" + + with patch( + "homeassistant.components.husqvarna_automower_ble.PLATFORMS", [Platform.SENSOR] + ): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From ba4e7e50e0095cc6a499e3a16ca39475dd1b318a Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:10:30 +0200 Subject: [PATCH 125/265] Add friend tracking to PlayStation Network (#149546) --- .../playstation_network/__init__.py | 22 +- .../playstation_network/config_flow.py | 94 ++++++- .../components/playstation_network/const.py | 1 + .../playstation_network/coordinator.py | 91 ++++++- .../components/playstation_network/entity.py | 24 +- .../components/playstation_network/helpers.py | 18 +- .../components/playstation_network/icons.json | 7 + .../components/playstation_network/image.py | 60 +++- .../components/playstation_network/sensor.py | 62 ++++- .../playstation_network/strings.json | 41 +++ .../playstation_network/conftest.py | 23 +- .../snapshots/test_diagnostics.ambr | 1 - .../snapshots/test_sensor.ambr | 256 ++++++++++++++++++ .../playstation_network/test_config_flow.py | 172 +++++++++++- .../playstation_network/test_init.py | 81 ++++++ 15 files changed, 920 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/playstation_network/__init__.py b/homeassistant/components/playstation_network/__init__.py index bfa9de5d5cb..c2399c61f93 100644 --- a/homeassistant/components/playstation_network/__init__.py +++ b/homeassistant/components/playstation_network/__init__.py @@ -8,6 +8,7 @@ from homeassistant.core import HomeAssistant from .const import CONF_NPSSO from .coordinator import ( PlaystationNetworkConfigEntry, + PlaystationNetworkFriendDataCoordinator, PlaystationNetworkGroupsUpdateCoordinator, PlaystationNetworkRuntimeData, PlaystationNetworkTrophyTitlesCoordinator, @@ -39,14 +40,33 @@ async def async_setup_entry( groups = PlaystationNetworkGroupsUpdateCoordinator(hass, psn, entry) await groups.async_config_entry_first_refresh() + friends = {} + + for subentry_id, subentry in entry.subentries.items(): + friend_coordinator = PlaystationNetworkFriendDataCoordinator( + hass, psn, entry, subentry + ) + await friend_coordinator.async_config_entry_first_refresh() + friends[subentry_id] = friend_coordinator + entry.runtime_data = PlaystationNetworkRuntimeData( - coordinator, trophy_titles, groups + coordinator, trophy_titles, groups, friends ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True +async def _async_update_listener( + hass: HomeAssistant, entry: PlaystationNetworkConfigEntry +) -> None: + """Handle update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry( hass: HomeAssistant, entry: PlaystationNetworkConfigEntry ) -> bool: diff --git a/homeassistant/components/playstation_network/config_flow.py b/homeassistant/components/playstation_network/config_flow.py index 0e69abf1080..d4822225c61 100644 --- a/homeassistant/components/playstation_network/config_flow.py +++ b/homeassistant/components/playstation_network/config_flow.py @@ -10,13 +10,28 @@ from psnawp_api.core.psnawp_exceptions import ( PSNAWPInvalidTokenError, PSNAWPNotFoundError, ) +from psnawp_api.models import User from psnawp_api.utils.misc import parse_npsso_token import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + ConfigSubentryFlow, + SubentryFlowResult, +) from homeassistant.const import CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, +) -from .const import CONF_NPSSO, DOMAIN, NPSSO_LINK, PSN_LINK +from .const import CONF_ACCOUNT_ID, CONF_NPSSO, DOMAIN, NPSSO_LINK, PSN_LINK +from .coordinator import PlaystationNetworkConfigEntry from .helpers import PlaystationNetwork _LOGGER = logging.getLogger(__name__) @@ -27,6 +42,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_NPSSO): str}) class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Playstation Network.""" + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: ConfigEntry + ) -> dict[str, type[ConfigSubentryFlow]]: + """Return subentries supported by this integration.""" + return {"friend": FriendSubentryFlowHandler} + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -54,6 +77,15 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN): else: await self.async_set_unique_id(user.account_id) self._abort_if_unique_id_configured() + config_entries = self.hass.config_entries.async_entries(DOMAIN) + for entry in config_entries: + if user.account_id in { + subentry.unique_id for subentry in entry.subentries.values() + }: + return self.async_abort( + reason="already_configured_as_subentry" + ) + return self.async_create_entry( title=user.online_id, data={CONF_NPSSO: npsso}, @@ -132,3 +164,61 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN): "psn_link": PSN_LINK, }, ) + + +class FriendSubentryFlowHandler(ConfigSubentryFlow): + """Handle subentry flow for adding a friend.""" + + friends_list: dict[str, User] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> SubentryFlowResult: + """Subentry user flow.""" + config_entry: PlaystationNetworkConfigEntry = self._get_entry() + + if user_input is not None: + config_entries = self.hass.config_entries.async_entries(DOMAIN) + if user_input[CONF_ACCOUNT_ID] in { + entry.unique_id for entry in config_entries + }: + return self.async_abort(reason="already_configured_as_entry") + for entry in config_entries: + if user_input[CONF_ACCOUNT_ID] in { + subentry.unique_id for subentry in entry.subentries.values() + }: + return self.async_abort(reason="already_configured") + + return self.async_create_entry( + title=self.friends_list[user_input[CONF_ACCOUNT_ID]].online_id, + data={}, + unique_id=user_input[CONF_ACCOUNT_ID], + ) + + self.friends_list = await self.hass.async_add_executor_job( + lambda: { + friend.account_id: friend + for friend in config_entry.runtime_data.user_data.psn.user.friends_list() + } + ) + + options = [ + SelectOptionDict( + value=friend.account_id, + label=friend.online_id, + ) + for friend in self.friends_list.values() + ] + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_ACCOUNT_ID): SelectSelector( + SelectSelectorConfig(options=options) + ) + } + ), + user_input, + ), + ) diff --git a/homeassistant/components/playstation_network/const.py b/homeassistant/components/playstation_network/const.py index f4c5c7a3e5b..df553a2ec01 100644 --- a/homeassistant/components/playstation_network/const.py +++ b/homeassistant/components/playstation_network/const.py @@ -6,6 +6,7 @@ from psnawp_api.models.trophies import PlatformType DOMAIN = "playstation_network" CONF_NPSSO: Final = "npsso" +CONF_ACCOUNT_ID: Final = "account_id" SUPPORTED_PLATFORMS = { PlatformType.PS_VITA, diff --git a/homeassistant/components/playstation_network/coordinator.py b/homeassistant/components/playstation_network/coordinator.py index 19153d1bb01..c447e8dc503 100644 --- a/homeassistant/components/playstation_network/coordinator.py +++ b/homeassistant/components/playstation_network/coordinator.py @@ -6,21 +6,30 @@ from abc import abstractmethod from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any from psnawp_api.core.psnawp_exceptions import ( PSNAWPAuthenticationError, PSNAWPClientError, + PSNAWPError, + PSNAWPForbiddenError, + PSNAWPNotFoundError, PSNAWPServerError, ) +from psnawp_api.models import User from psnawp_api.models.group.group_datatypes import GroupDetails from psnawp_api.models.trophies import TrophyTitle -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigSubentry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN +from .const import CONF_ACCOUNT_ID, DOMAIN from .helpers import PlaystationNetwork, PlaystationNetworkData _LOGGER = logging.getLogger(__name__) @@ -35,6 +44,7 @@ class PlaystationNetworkRuntimeData: user_data: PlaystationNetworkUserDataCoordinator trophy_titles: PlaystationNetworkTrophyTitlesCoordinator groups: PlaystationNetworkGroupsUpdateCoordinator + friends: dict[str, PlaystationNetworkFriendDataCoordinator] class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]): @@ -140,3 +150,78 @@ class PlaystationNetworkGroupsUpdateCoordinator( if not group_info.group_id.startswith("~") } ) + + +class PlaystationNetworkFriendDataCoordinator( + PlayStationNetworkBaseCoordinator[PlaystationNetworkData] +): + """Friend status data update coordinator for PSN.""" + + user: User + profile: dict[str, Any] + + def __init__( + self, + hass: HomeAssistant, + psn: PlaystationNetwork, + config_entry: PlaystationNetworkConfigEntry, + subentry: ConfigSubentry, + ) -> None: + """Initialize the Coordinator.""" + self._update_interval = timedelta( + seconds=max(9 * len(config_entry.subentries), 180) + ) + super().__init__(hass, psn, config_entry) + self.subentry = subentry + + def _setup(self) -> None: + """Set up the coordinator.""" + self.user = self.psn.psn.user(account_id=self.subentry.data[CONF_ACCOUNT_ID]) + self.profile = self.user.profile() + + async def _async_setup(self) -> None: + """Set up the coordinator.""" + + try: + await self.hass.async_add_executor_job(self._setup) + except PSNAWPNotFoundError as error: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="user_not_found", + translation_placeholders={"user": self.subentry.title}, + ) from error + + except PSNAWPAuthenticationError as error: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="not_ready", + ) from error + + except (PSNAWPServerError, PSNAWPClientError) as error: + _LOGGER.debug("Update failed", exc_info=True) + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="update_failed", + ) from error + + def _update_data(self) -> PlaystationNetworkData: + """Update friend status data.""" + try: + return PlaystationNetworkData( + username=self.user.online_id, + account_id=self.user.account_id, + presence=self.user.get_presence(), + profile=self.profile, + ) + except PSNAWPForbiddenError as error: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="user_profile_private", + translation_placeholders={"user": self.subentry.title}, + ) from error + except PSNAWPError: + raise + + async def update_data(self) -> PlaystationNetworkData: + """Update friend status data.""" + return await self.hass.async_add_executor_job(self._update_data) diff --git a/homeassistant/components/playstation_network/entity.py b/homeassistant/components/playstation_network/entity.py index ad7c52bdb39..dc1f126505c 100644 --- a/homeassistant/components/playstation_network/entity.py +++ b/homeassistant/components/playstation_network/entity.py @@ -2,12 +2,14 @@ from typing import TYPE_CHECKING +from homeassistant.config_entries import ConfigSubentry from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import PlayStationNetworkBaseCoordinator +from .helpers import PlaystationNetworkData class PlaystationNetworkServiceEntity( @@ -21,18 +23,32 @@ class PlaystationNetworkServiceEntity( self, coordinator: PlayStationNetworkBaseCoordinator, entity_description: EntityDescription, + subentry: ConfigSubentry | None = None, ) -> None: """Initialize PlayStation Network Service Entity.""" super().__init__(coordinator) if TYPE_CHECKING: assert coordinator.config_entry.unique_id self.entity_description = entity_description - self._attr_unique_id = ( - f"{coordinator.config_entry.unique_id}_{entity_description.key}" + self.subentry = subentry + unique_id = ( + subentry.unique_id + if subentry is not None and subentry.unique_id + else coordinator.config_entry.unique_id ) + + self._attr_unique_id = f"{unique_id}_{entity_description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.config_entry.unique_id)}, - name=coordinator.psn.user.online_id, + identifiers={(DOMAIN, unique_id)}, + name=( + coordinator.data.username + if isinstance(coordinator.data, PlaystationNetworkData) + else coordinator.psn.user.online_id + ), entry_type=DeviceEntryType.SERVICE, manufacturer="Sony Interactive Entertainment", ) + if subentry: + self._attr_device_info.update( + DeviceInfo(via_device=(DOMAIN, coordinator.config_entry.unique_id)) + ) diff --git a/homeassistant/components/playstation_network/helpers.py b/homeassistant/components/playstation_network/helpers.py index 9960d8afd79..492a011cf78 100644 --- a/homeassistant/components/playstation_network/helpers.py +++ b/homeassistant/components/playstation_network/helpers.py @@ -38,7 +38,6 @@ class PlaystationNetworkData: presence: dict[str, Any] = field(default_factory=dict) username: str = "" account_id: str = "" - availability: str = "unavailable" active_sessions: dict[PlatformType, SessionData] = field(default_factory=dict) registered_platforms: set[PlatformType] = field(default_factory=set) trophy_summary: TrophySummary | None = None @@ -61,6 +60,7 @@ class PlaystationNetwork: self.legacy_profile: dict[str, Any] | None = None self.trophy_titles: list[TrophyTitle] = [] self._title_icon_urls: dict[str, str] = {} + self.friends_list: dict[str, User] | None = None def _setup(self) -> None: """Setup PSN.""" @@ -97,6 +97,7 @@ class PlaystationNetwork: # check legacy platforms if owned if LEGACY_PLATFORMS & data.registered_platforms: self.legacy_profile = self.client.get_profile_legacy() + return data async def get_data(self) -> PlaystationNetworkData: @@ -105,7 +106,6 @@ class PlaystationNetwork: data.username = self.user.online_id data.account_id = self.user.account_id data.shareable_profile_link = self.shareable_profile_link - data.availability = data.presence["basicPresence"]["availability"] if "platform" in data.presence["basicPresence"]["primaryPlatformInfo"]: primary_platform = PlatformType( @@ -193,3 +193,17 @@ class PlaystationNetwork: def normalize_title(name: str) -> str: """Normalize trophy title.""" return name.removesuffix("Trophies").removesuffix("Trophy Set").strip() + + +def get_game_title_info(presence: dict[str, Any]) -> dict[str, Any]: + """Retrieve title info from presence.""" + + return ( + next((title for title in game_title_info), {}) + if ( + game_title_info := presence.get("basicPresence", {}).get( + "gameTitleInfoList" + ) + ) + else {} + ) diff --git a/homeassistant/components/playstation_network/icons.json b/homeassistant/components/playstation_network/icons.json index af2236bd126..5997f43fb5c 100644 --- a/homeassistant/components/playstation_network/icons.json +++ b/homeassistant/components/playstation_network/icons.json @@ -42,6 +42,13 @@ "availabletocommunicate": "mdi:cellphone", "offline": "mdi:account-off-outline" } + }, + "now_playing": { + "default": "mdi:controller", + "state": { + "unknown": "mdi:controller-off", + "unavailable": "mdi:controller-off" + } } }, "image": { diff --git a/homeassistant/components/playstation_network/image.py b/homeassistant/components/playstation_network/image.py index b0195002c66..0a8e5daed62 100644 --- a/homeassistant/components/playstation_network/image.py +++ b/homeassistant/components/playstation_network/image.py @@ -5,18 +5,23 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from enum import StrEnum +from typing import TYPE_CHECKING from homeassistant.components.image import ImageEntity, ImageEntityDescription +from homeassistant.config_entries import ConfigSubentry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util from .coordinator import ( + PlayStationNetworkBaseCoordinator, PlaystationNetworkConfigEntry, PlaystationNetworkData, + PlaystationNetworkFriendDataCoordinator, PlaystationNetworkUserDataCoordinator, ) from .entity import PlaystationNetworkServiceEntity +from .helpers import get_game_title_info PARALLEL_UPDATES = 0 @@ -26,6 +31,7 @@ class PlaystationNetworkImage(StrEnum): AVATAR = "avatar" SHARE_PROFILE = "share_profile" + NOW_PLAYING_IMAGE = "now_playing_image" @dataclass(kw_only=True, frozen=True) @@ -35,12 +41,14 @@ class PlaystationNetworkImageEntityDescription(ImageEntityDescription): image_url_fn: Callable[[PlaystationNetworkData], str | None] -IMAGE_DESCRIPTIONS: tuple[PlaystationNetworkImageEntityDescription, ...] = ( +IMAGE_DESCRIPTIONS_ME: tuple[PlaystationNetworkImageEntityDescription, ...] = ( PlaystationNetworkImageEntityDescription( key=PlaystationNetworkImage.SHARE_PROFILE, translation_key=PlaystationNetworkImage.SHARE_PROFILE, image_url_fn=lambda data: data.shareable_profile_link["shareImageUrl"], ), +) +IMAGE_DESCRIPTIONS_ALL: tuple[PlaystationNetworkImageEntityDescription, ...] = ( PlaystationNetworkImageEntityDescription( key=PlaystationNetworkImage.AVATAR, translation_key=PlaystationNetworkImage.AVATAR, @@ -55,6 +63,14 @@ IMAGE_DESCRIPTIONS: tuple[PlaystationNetworkImageEntityDescription, ...] = ( ) ), ), + PlaystationNetworkImageEntityDescription( + key=PlaystationNetworkImage.NOW_PLAYING_IMAGE, + translation_key=PlaystationNetworkImage.NOW_PLAYING_IMAGE, + image_url_fn=( + lambda data: get_game_title_info(data.presence).get("conceptIconUrl") + or get_game_title_info(data.presence).get("npTitleIconUrl") + ), + ), ) @@ -70,25 +86,43 @@ async def async_setup_entry( async_add_entities( [ PlaystationNetworkImageEntity(hass, coordinator, description) - for description in IMAGE_DESCRIPTIONS + for description in IMAGE_DESCRIPTIONS_ME + IMAGE_DESCRIPTIONS_ALL ] ) + for ( + subentry_id, + friend_data_coordinator, + ) in config_entry.runtime_data.friends.items(): + async_add_entities( + [ + PlaystationNetworkFriendImageEntity( + hass, + friend_data_coordinator, + description, + config_entry.subentries[subentry_id], + ) + for description in IMAGE_DESCRIPTIONS_ALL + ], + config_subentry_id=subentry_id, + ) -class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity): + +class PlaystationNetworkImageBaseEntity(PlaystationNetworkServiceEntity, ImageEntity): """An image entity.""" entity_description: PlaystationNetworkImageEntityDescription - coordinator: PlaystationNetworkUserDataCoordinator + coordinator: PlayStationNetworkBaseCoordinator def __init__( self, hass: HomeAssistant, - coordinator: PlaystationNetworkUserDataCoordinator, + coordinator: PlayStationNetworkBaseCoordinator, entity_description: PlaystationNetworkImageEntityDescription, + subentry: ConfigSubentry | None = None, ) -> None: """Initialize the image entity.""" - super().__init__(coordinator, entity_description) + super().__init__(coordinator, entity_description, subentry) ImageEntity.__init__(self, hass) self._attr_image_url = self.entity_description.image_url_fn(coordinator.data) @@ -96,6 +130,8 @@ class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" + if TYPE_CHECKING: + assert isinstance(self.coordinator.data, PlaystationNetworkData) url = self.entity_description.image_url_fn(self.coordinator.data) if url != self._attr_image_url: @@ -104,3 +140,15 @@ class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity self._attr_image_last_updated = dt_util.utcnow() super()._handle_coordinator_update() + + +class PlaystationNetworkImageEntity(PlaystationNetworkImageBaseEntity): + """An image entity.""" + + coordinator: PlaystationNetworkUserDataCoordinator + + +class PlaystationNetworkFriendImageEntity(PlaystationNetworkImageBaseEntity): + """An image entity.""" + + coordinator: PlaystationNetworkFriendDataCoordinator diff --git a/homeassistant/components/playstation_network/sensor.py b/homeassistant/components/playstation_network/sensor.py index 63cca074c3e..16d1ff13906 100644 --- a/homeassistant/components/playstation_network/sensor.py +++ b/homeassistant/components/playstation_network/sensor.py @@ -19,11 +19,14 @@ from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from .coordinator import ( + PlayStationNetworkBaseCoordinator, PlaystationNetworkConfigEntry, PlaystationNetworkData, + PlaystationNetworkFriendDataCoordinator, PlaystationNetworkUserDataCoordinator, ) from .entity import PlaystationNetworkServiceEntity +from .helpers import get_game_title_info PARALLEL_UPDATES = 0 @@ -33,7 +36,6 @@ class PlaystationNetworkSensorEntityDescription(SensorEntityDescription): """PlayStation Network sensor description.""" value_fn: Callable[[PlaystationNetworkData], StateType | datetime] - entity_picture: str | None = None available_fn: Callable[[PlaystationNetworkData], bool] = lambda _: True @@ -49,9 +51,10 @@ class PlaystationNetworkSensor(StrEnum): ONLINE_ID = "online_id" LAST_ONLINE = "last_online" ONLINE_STATUS = "online_status" + NOW_PLAYING = "now_playing" -SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( +SENSOR_DESCRIPTIONS_TROPHY: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( PlaystationNetworkSensorEntityDescription( key=PlaystationNetworkSensor.TROPHY_LEVEL, translation_key=PlaystationNetworkSensor.TROPHY_LEVEL, @@ -103,6 +106,8 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( else None ), ), +) +SENSOR_DESCRIPTIONS_USER: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( PlaystationNetworkSensorEntityDescription( key=PlaystationNetworkSensor.ONLINE_ID, translation_key=PlaystationNetworkSensor.ONLINE_ID, @@ -122,10 +127,19 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( PlaystationNetworkSensorEntityDescription( key=PlaystationNetworkSensor.ONLINE_STATUS, translation_key=PlaystationNetworkSensor.ONLINE_STATUS, - value_fn=lambda psn: psn.availability.lower().replace("unavailable", "offline"), + value_fn=( + lambda psn: psn.presence["basicPresence"]["availability"] + .lower() + .replace("unavailable", "offline") + ), device_class=SensorDeviceClass.ENUM, options=["offline", "availabletoplay", "availabletocommunicate", "busy"], ), + PlaystationNetworkSensorEntityDescription( + key=PlaystationNetworkSensor.NOW_PLAYING, + translation_key=PlaystationNetworkSensor.NOW_PLAYING, + value_fn=lambda psn: get_game_title_info(psn.presence).get("titleName"), + ), ) @@ -138,18 +152,34 @@ async def async_setup_entry( coordinator = config_entry.runtime_data.user_data async_add_entities( PlaystationNetworkSensorEntity(coordinator, description) - for description in SENSOR_DESCRIPTIONS + for description in SENSOR_DESCRIPTIONS_TROPHY + SENSOR_DESCRIPTIONS_USER ) + for ( + subentry_id, + friend_data_coordinator, + ) in config_entry.runtime_data.friends.items(): + async_add_entities( + [ + PlaystationNetworkFriendSensorEntity( + friend_data_coordinator, + description, + config_entry.subentries[subentry_id], + ) + for description in SENSOR_DESCRIPTIONS_USER + ], + config_subentry_id=subentry_id, + ) -class PlaystationNetworkSensorEntity( + +class PlaystationNetworkSensorBaseEntity( PlaystationNetworkServiceEntity, SensorEntity, ): - """Representation of a PlayStation Network sensor entity.""" + """Base sensor entity.""" entity_description: PlaystationNetworkSensorEntityDescription - coordinator: PlaystationNetworkUserDataCoordinator + coordinator: PlayStationNetworkBaseCoordinator @property def native_value(self) -> StateType | datetime: @@ -169,14 +199,24 @@ class PlaystationNetworkSensorEntity( (pic.get("url") for pic in profile_pictures if pic.get("size") == "xl"), None, ) - return super().entity_picture @property def available(self) -> bool: """Return True if entity is available.""" - return ( - self.entity_description.available_fn(self.coordinator.data) - and super().available + return super().available and self.entity_description.available_fn( + self.coordinator.data ) + + +class PlaystationNetworkSensorEntity(PlaystationNetworkSensorBaseEntity): + """Representation of a PlayStation Network sensor entity.""" + + coordinator: PlaystationNetworkUserDataCoordinator + + +class PlaystationNetworkFriendSensorEntity(PlaystationNetworkSensorBaseEntity): + """Representation of a PlayStation Network sensor entity.""" + + coordinator: PlaystationNetworkFriendDataCoordinator diff --git a/homeassistant/components/playstation_network/strings.json b/homeassistant/components/playstation_network/strings.json index 4fefc508ea2..e5192f42873 100644 --- a/homeassistant/components/playstation_network/strings.json +++ b/homeassistant/components/playstation_network/strings.json @@ -39,11 +39,40 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_configured_as_subentry": "Already configured as a friend for another account. Delete the existing entry first.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unique_id_mismatch": "The provided NPSSO token corresponds to the account {wrong_account}. Please re-authenticate with the account **{name}**", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, + "config_subentries": { + "friend": { + "step": { + "user": { + "title": "Friend online status", + "description": "Track the online status of a PlayStation Network friend.", + "data": { + "account_id": "Online ID" + }, + "data_description": { + "account_id": "Select a friend from your friend list to track their online status." + } + } + }, + "initiate_flow": { + "user": "Add friend" + }, + "entry_type": "Friend", + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured_as_entry": "Already configured as a service. This account cannot be added as a friend.", + "already_configured": "Already configured as a friend in this or another account." + } + } + }, "exceptions": { "not_ready": { "message": "Authentication to the PlayStation Network failed." @@ -59,6 +88,12 @@ }, "send_message_failed": { "message": "Failed to send message to group {group_name}. Try again later." + }, + "user_profile_private": { + "message": "Unable to retrieve data for {user}. Privacy settings restrict access to activity." + }, + "user_not_found": { + "message": "Unable to retrieve data for {user}. User does not exist or has been removed." } }, "entity": { @@ -104,6 +139,9 @@ "availabletocommunicate": "Online on PS App", "busy": "Away" } + }, + "now_playing": { + "name": "Now playing" } }, "image": { @@ -112,6 +150,9 @@ }, "avatar": { "name": "Avatar" + }, + "now_playing_image": { + "name": "[%key:component::playstation_network::entity::sensor::now_playing::name%]" } }, "notify": { diff --git a/tests/components/playstation_network/conftest.py b/tests/components/playstation_network/conftest.py index 8480d7ecf5d..ab4edc0e3f4 100644 --- a/tests/components/playstation_network/conftest.py +++ b/tests/components/playstation_network/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, MagicMock, patch +from psnawp_api.models import User from psnawp_api.models.group.group import Group from psnawp_api.models.trophies import ( PlatformType, @@ -13,7 +14,12 @@ from psnawp_api.models.trophies import ( ) import pytest -from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN +from homeassistant.components.playstation_network.const import ( + CONF_ACCOUNT_ID, + CONF_NPSSO, + DOMAIN, +) +from homeassistant.config_entries import ConfigSubentryData from tests.common import MockConfigEntry @@ -32,6 +38,15 @@ def mock_config_entry() -> MockConfigEntry: CONF_NPSSO: NPSSO_TOKEN, }, unique_id=PSN_ID, + subentries_data=[ + ConfigSubentryData( + data={CONF_ACCOUNT_ID: "fren-psn-id"}, + subentry_id="ABCDEF", + subentry_type="friend", + title="PublicUniversalFriend", + unique_id="fren-psn-id", + ) + ], ) @@ -170,6 +185,12 @@ def mock_psnawpapi(mock_user: MagicMock) -> Generator[MagicMock]: ], } client.me.return_value.get_groups.return_value = [group] + fren = MagicMock( + spec=User, account_id="fren-psn-id", online_id="PublicUniversalFriend" + ) + + client.user.return_value.friends_list.return_value = [fren] + yield client diff --git a/tests/components/playstation_network/snapshots/test_diagnostics.ambr b/tests/components/playstation_network/snapshots/test_diagnostics.ambr index 894fa2d9084..ca5e9f98628 100644 --- a/tests/components/playstation_network/snapshots/test_diagnostics.ambr +++ b/tests/components/playstation_network/snapshots/test_diagnostics.ambr @@ -21,7 +21,6 @@ 'title_name': "Assassin's Creed® III Liberation", }), }), - 'availability': 'availableToPlay', 'presence': dict({ 'basicPresence': dict({ 'availability': 'availableToPlay', diff --git a/tests/components/playstation_network/snapshots/test_sensor.ambr b/tests/components/playstation_network/snapshots/test_sensor.ambr index a00e3c4ff0a..046989cebe6 100644 --- a/tests/components/playstation_network/snapshots/test_sensor.ambr +++ b/tests/components/playstation_network/snapshots/test_sensor.ambr @@ -146,6 +146,55 @@ 'state': '2025-06-30T01:42:15+00:00', }) # --- +# name: test_sensors[sensor.testuser_last_online_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_last_online_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last online', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_last_online', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_last_online_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'testuser Last online', + }), + 'context': , + 'entity_id': 'sensor.testuser_last_online_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-06-30T01:42:15+00:00', + }) +# --- # name: test_sensors[sensor.testuser_next_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -195,6 +244,102 @@ 'state': '19', }) # --- +# name: test_sensors[sensor.testuser_now_playing-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.testuser_now_playing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Now playing', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'my-psn-id_now_playing', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_now_playing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testuser Now playing', + }), + 'context': , + 'entity_id': 'sensor.testuser_now_playing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'STAR WARS Jedi: Survivor™', + }) +# --- +# name: test_sensors[sensor.testuser_now_playing_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_now_playing_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Now playing', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_now_playing', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_now_playing_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testuser Now playing', + }), + 'context': , + 'entity_id': 'sensor.testuser_now_playing_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'STAR WARS Jedi: Survivor™', + }) +# --- # name: test_sensors[sensor.testuser_online_id-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -244,6 +389,55 @@ 'state': 'testuser', }) # --- +# name: test_sensors[sensor.testuser_online_id_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_online_id_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Online ID', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_online_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_online_id_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'http://static-resource.np.community.playstation.net/avatar_xl/WWS_A/UP90001312L24_DD96EB6A4FF5FE883C09_XL.png', + 'friendly_name': 'testuser Online ID', + }), + 'context': , + 'entity_id': 'sensor.testuser_online_id_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'testuser', + }) +# --- # name: test_sensors[sensor.testuser_online_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -306,6 +500,68 @@ 'state': 'availabletoplay', }) # --- +# name: test_sensors[sensor.testuser_online_status_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'offline', + 'availabletoplay', + 'availabletocommunicate', + 'busy', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_online_status_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Online status', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_online_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_online_status_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'testuser Online status', + 'options': list([ + 'offline', + 'availabletoplay', + 'availabletocommunicate', + 'busy', + ]), + }), + 'context': , + 'entity_id': 'sensor.testuser_online_status_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'availabletoplay', + }) +# --- # name: test_sensors[sensor.testuser_platinum_trophies-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/playstation_network/test_config_flow.py b/tests/components/playstation_network/test_config_flow.py index dc3ad55c64f..4194f1fb258 100644 --- a/tests/components/playstation_network/test_config_flow.py +++ b/tests/components/playstation_network/test_config_flow.py @@ -10,8 +10,17 @@ from homeassistant.components.playstation_network.config_flow import ( PSNAWPInvalidTokenError, PSNAWPNotFoundError, ) -from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN -from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.components.playstation_network.const import ( + CONF_ACCOUNT_ID, + CONF_NPSSO, + DOMAIN, +) +from homeassistant.config_entries import ( + SOURCE_USER, + ConfigEntryState, + ConfigSubentry, + ConfigSubentryData, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -67,6 +76,45 @@ async def test_form_already_configured( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_psnawpapi") +async def test_form_already_configured_as_subentry(hass: HomeAssistant) -> None: + """Test we abort form login when entry is already configured as subentry of another entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="PublicUniversalFriend", + data={ + CONF_NPSSO: NPSSO_TOKEN, + }, + unique_id="fren-psn-id", + subentries_data=[ + ConfigSubentryData( + data={CONF_ACCOUNT_ID: PSN_ID}, + subentry_id="ABCDEF", + subentry_type="friend", + title="test-user", + unique_id=PSN_ID, + ) + ], + ) + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_NPSSO: NPSSO_TOKEN}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured_as_subentry" + + @pytest.mark.parametrize( ("raise_error", "text_error"), [ @@ -325,3 +373,123 @@ async def test_flow_reconfigure( assert config_entry.data[CONF_NPSSO] == "NEW_NPSSO_TOKEN" assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.usefixtures("mock_psnawpapi") +async def test_add_friend_flow(hass: HomeAssistant) -> None: + """Test add friend subentry flow.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title="test-user", + data={ + CONF_NPSSO: NPSSO_TOKEN, + }, + unique_id=PSN_ID, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.subentries.async_init( + (config_entry.entry_id, "friend"), + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={CONF_ACCOUNT_ID: "fren-psn-id"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + subentry_id = list(config_entry.subentries)[0] + assert config_entry.subentries == { + subentry_id: ConfigSubentry( + data={}, + subentry_id=subentry_id, + subentry_type="friend", + title="PublicUniversalFriend", + unique_id="fren-psn-id", + ) + } + + +@pytest.mark.usefixtures("mock_psnawpapi") +async def test_add_friend_flow_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test we abort add friend subentry flow when already configured.""" + + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.subentries.async_init( + (config_entry.entry_id, "friend"), + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={CONF_ACCOUNT_ID: "fren-psn-id"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_psnawpapi") +async def test_add_friend_flow_already_configured_as_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test we abort add friend subentry flow when already configured as config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="test-user", + data={ + CONF_NPSSO: NPSSO_TOKEN, + }, + unique_id=PSN_ID, + ) + fren_config_entry = MockConfigEntry( + domain=DOMAIN, + title="PublicUniversalFriend", + data={ + CONF_NPSSO: NPSSO_TOKEN, + }, + unique_id="fren-psn-id", + ) + + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + fren_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(fren_config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.subentries.async_init( + (config_entry.entry_id, "friend"), + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={CONF_ACCOUNT_ID: "fren-psn-id"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured_as_entry" diff --git a/tests/components/playstation_network/test_init.py b/tests/components/playstation_network/test_init.py index c1f2691d623..6db4cb6ab6a 100644 --- a/tests/components/playstation_network/test_init.py +++ b/tests/components/playstation_network/test_init.py @@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from psnawp_api.core import ( PSNAWPAuthenticationError, PSNAWPClientError, + PSNAWPForbiddenError, PSNAWPNotFoundError, PSNAWPServerError, ) @@ -263,3 +264,83 @@ async def test_trophy_title_coordinator_play_new_game( state.attributes["entity_picture"] == "https://image.api.playstation.com/trophy/np/NPWR03134_00_0008206095F67FD3BB385E9E00A7C9CFE6F5A4AB96/5F87A6997DD23D1C4D4CC0D1F958ED79CB905331.PNG" ) + + +@pytest.mark.parametrize( + "exception", + [PSNAWPNotFoundError, PSNAWPServerError, PSNAWPClientError, PSNAWPForbiddenError], +) +async def test_friends_coordinator_update_data_failed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_psnawpapi: MagicMock, + exception: Exception, +) -> None: + """Test friends coordinator setup fails in _update_data.""" + + mock_psnawpapi.user.return_value.get_presence.side_effect = [ + mock_psnawpapi.user.return_value.get_presence.return_value, + exception, + ] + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + ("exception", "state"), + [ + (PSNAWPNotFoundError, ConfigEntryState.SETUP_ERROR), + (PSNAWPAuthenticationError, ConfigEntryState.SETUP_ERROR), + (PSNAWPServerError, ConfigEntryState.SETUP_RETRY), + (PSNAWPClientError, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_friends_coordinator_setup_failed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_psnawpapi: MagicMock, + exception: Exception, + state: ConfigEntryState, +) -> None: + """Test friends coordinator setup fails in _async_setup.""" + + mock_psnawpapi.user.side_effect = [ + mock_psnawpapi.user.return_value, + exception, + ] + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is state + + +async def test_friends_coordinator_auth_failed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_psnawpapi: MagicMock, +) -> None: + """Test friends coordinator starts reauth on authentication error.""" + mock_psnawpapi.user.side_effect = [ + mock_psnawpapi.user.return_value, + PSNAWPAuthenticationError, + ] + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == config_entry.entry_id From c4d4ef884e6276bcb8e321d2eae73246661ea888 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:13:39 +0200 Subject: [PATCH 126/265] Add hassio discovery flow to Uptime Kuma (#148770) --- .../components/uptime_kuma/config_flow.py | 63 +++++- .../components/uptime_kuma/quality_scale.yaml | 8 +- .../components/uptime_kuma/strings.json | 10 + tests/components/uptime_kuma/conftest.py | 11 + .../uptime_kuma/test_config_flow.py | 202 +++++++++++++++++- 5 files changed, 287 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/uptime_kuma/config_flow.py b/homeassistant/components/uptime_kuma/config_flow.py index da71084d1bc..a6429ea7dfe 100644 --- a/homeassistant/components/uptime_kuma/config_flow.py +++ b/homeassistant/components/uptime_kuma/config_flow.py @@ -23,6 +23,7 @@ from homeassistant.helpers.selector import ( TextSelectorConfig, TextSelectorType, ) +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .const import DOMAIN @@ -47,7 +48,7 @@ async def validate_connection( hass: HomeAssistant, url: URL | str, verify_ssl: bool, - api_key: str, + api_key: str | None, ) -> dict[str, str]: """Validate Uptime Kuma connectivity.""" errors: dict[str, str] = {} @@ -69,6 +70,8 @@ async def validate_connection( class UptimeKumaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Uptime Kuma.""" + _hassio_discovery: HassioServiceInfo | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -168,3 +171,61 @@ class UptimeKumaConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: + """Prepare configuration for Uptime Kuma add-on. + + This flow is triggered by the discovery component. + """ + self._async_abort_entries_match({CONF_URL: discovery_info.config[CONF_URL]}) + await self.async_set_unique_id(discovery_info.uuid) + self._abort_if_unique_id_configured( + updates={CONF_URL: discovery_info.config[CONF_URL]} + ) + + self._hassio_discovery = discovery_info + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm Supervisor discovery.""" + assert self._hassio_discovery + errors: dict[str, str] = {} + api_key = user_input[CONF_API_KEY] if user_input else None + + if not ( + errors := await validate_connection( + self.hass, + self._hassio_discovery.config[CONF_URL], + True, + api_key, + ) + ): + if user_input is None: + self._set_confirm_only() + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={ + "addon": self._hassio_discovery.config["addon"] + }, + ) + return self.async_create_entry( + title=self._hassio_discovery.slug, + data={ + CONF_URL: self._hassio_discovery.config[CONF_URL], + CONF_VERIFY_SSL: True, + CONF_API_KEY: api_key, + }, + ) + + return self.async_show_form( + step_id="hassio_confirm", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_REAUTH_DATA_SCHEMA, suggested_values=user_input + ), + description_placeholders={"addon": self._hassio_discovery.config["addon"]}, + errors=errors if user_input is not None else None, + ) diff --git a/homeassistant/components/uptime_kuma/quality_scale.yaml b/homeassistant/components/uptime_kuma/quality_scale.yaml index 876318c8917..3c9b5a3af50 100644 --- a/homeassistant/components/uptime_kuma/quality_scale.yaml +++ b/homeassistant/components/uptime_kuma/quality_scale.yaml @@ -44,12 +44,10 @@ rules: # Gold devices: done diagnostics: done - discovery-update-info: - status: exempt - comment: is not locally discoverable + discovery-update-info: done discovery: - status: exempt - comment: is not locally discoverable + status: done + comment: hassio addon supports discovery, other installation methods are not discoverable docs-data-update: done docs-examples: todo docs-known-limitations: done diff --git a/homeassistant/components/uptime_kuma/strings.json b/homeassistant/components/uptime_kuma/strings.json index 62b1ccbdd9a..e84b68501f3 100644 --- a/homeassistant/components/uptime_kuma/strings.json +++ b/homeassistant/components/uptime_kuma/strings.json @@ -36,6 +36,16 @@ "verify_ssl": "[%key:component::uptime_kuma::config::step::user::data_description::verify_ssl%]", "api_key": "[%key:component::uptime_kuma::config::step::user::data_description::api_key%]" } + }, + "hassio_confirm": { + "title": "Uptime Kuma via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the Uptime Kuma service provided by the add-on: {addon}?", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::uptime_kuma::config::step::user::data_description::api_key%]" + } } }, "error": { diff --git a/tests/components/uptime_kuma/conftest.py b/tests/components/uptime_kuma/conftest.py index 7895f068b31..a092c2e85ba 100644 --- a/tests/components/uptime_kuma/conftest.py +++ b/tests/components/uptime_kuma/conftest.py @@ -10,9 +10,20 @@ from pythonkuma.update import LatestRelease from homeassistant.components.uptime_kuma.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from tests.common import MockConfigEntry +ADDON_SERVICE_INFO = HassioServiceInfo( + config={ + "addon": "Uptime Kuma", + CONF_URL: "http://localhost:3001/", + }, + name="Uptime Kuma", + slug="a0d7b954_uptime-kuma", + uuid="1234", +) + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: diff --git a/tests/components/uptime_kuma/test_config_flow.py b/tests/components/uptime_kuma/test_config_flow.py index ab695107b9b..b8b40a5b759 100644 --- a/tests/components/uptime_kuma/test_config_flow.py +++ b/tests/components/uptime_kuma/test_config_flow.py @@ -6,11 +6,13 @@ import pytest from pythonkuma import UptimeKumaAuthenticationException, UptimeKumaConnectionException from homeassistant.components.uptime_kuma.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import ADDON_SERVICE_INFO + from tests.common import MockConfigEntry @@ -280,3 +282,201 @@ async def test_flow_reconfigure_errors( } assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pythonkuma: AsyncMock, +) -> None: + """Test config flow initiated by Supervisor.""" + mock_pythonkuma.metrics.side_effect = [UptimeKumaAuthenticationException, None] + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + assert result["description_placeholders"] == {"addon": "Uptime Kuma"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "apikey"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "a0d7b954_uptime-kuma" + assert result["data"] == { + CONF_URL: "http://localhost:3001/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: "apikey", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery_confirm_only( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test config flow initiated by Supervisor. + + Config flow will first try to configure without authentication and if it + fails will show the form. + """ + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + assert result["description_placeholders"] == {"addon": "Uptime Kuma"} + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "a0d7b954_uptime-kuma" + assert result["data"] == { + CONF_URL: "http://localhost:3001/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: None, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery_already_configured( + hass: HomeAssistant, +) -> None: + """Test config flow initiated by Supervisor.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_URL: "http://localhost:3001/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: "apikey", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (UptimeKumaConnectionException, "cannot_connect"), + (UptimeKumaAuthenticationException, "invalid_auth"), + (ValueError, "unknown"), + ], +) +async def test_hassio_addon_discovery_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pythonkuma: AsyncMock, + raise_error: Exception, + text_error: str, +) -> None: + """Test we handle errors and recover.""" + mock_pythonkuma.metrics.side_effect = UptimeKumaAuthenticationException + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + mock_pythonkuma.metrics.side_effect = raise_error + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "apikey"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": text_error} + + mock_pythonkuma.metrics.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "apikey"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "a0d7b954_uptime-kuma" + assert result["data"] == { + CONF_URL: "http://localhost:3001/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: "apikey", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery_ignored( + hass: HomeAssistant, +) -> None: + """Test we abort discovery flow if discovery was ignored.""" + + MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IGNORE, + data={}, + entry_id="123456789", + unique_id="1234", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery_update_info( + hass: HomeAssistant, +) -> None: + """Test we abort discovery flow if already configured and we update from discovery info.""" + + entry = MockConfigEntry( + domain=DOMAIN, + title="a0d7b954_uptime-kuma", + data={ + CONF_URL: "http://localhost:80/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: "apikey", + }, + entry_id="123456789", + unique_id="1234", + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert entry.data[CONF_URL] == "http://localhost:3001/" From a5b075af68347d629e65523923007f30c2c622e2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 30 Jul 2025 15:20:23 +0200 Subject: [PATCH 127/265] Add climate support for MQTT subentries (#149451) Co-authored-by: Norbert Rittel --- homeassistant/components/mqtt/climate.py | 86 +- homeassistant/components/mqtt/config_flow.py | 802 ++++++++++++++++++- homeassistant/components/mqtt/const.py | 37 +- homeassistant/components/mqtt/strings.json | 231 ++++++ tests/components/mqtt/common.py | 123 +++ tests/components/mqtt/test_climate.py | 4 +- tests/components/mqtt/test_config_flow.py | 362 ++++++++- 7 files changed, 1580 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 931a57a71cc..52db0bd25da 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -60,6 +60,17 @@ from .const import ( CONF_CURRENT_HUMIDITY_TOPIC, CONF_CURRENT_TEMP_TEMPLATE, CONF_CURRENT_TEMP_TOPIC, + CONF_FAN_MODE_COMMAND_TEMPLATE, + CONF_FAN_MODE_COMMAND_TOPIC, + CONF_FAN_MODE_LIST, + CONF_FAN_MODE_STATE_TEMPLATE, + CONF_FAN_MODE_STATE_TOPIC, + CONF_HUMIDITY_COMMAND_TEMPLATE, + CONF_HUMIDITY_COMMAND_TOPIC, + CONF_HUMIDITY_MAX, + CONF_HUMIDITY_MIN, + CONF_HUMIDITY_STATE_TEMPLATE, + CONF_HUMIDITY_STATE_TOPIC, CONF_MODE_COMMAND_TEMPLATE, CONF_MODE_COMMAND_TOPIC, CONF_MODE_LIST, @@ -68,14 +79,39 @@ from .const import ( CONF_POWER_COMMAND_TEMPLATE, CONF_POWER_COMMAND_TOPIC, CONF_PRECISION, + CONF_PRESET_MODE_COMMAND_TEMPLATE, + CONF_PRESET_MODE_COMMAND_TOPIC, + CONF_PRESET_MODE_STATE_TOPIC, + CONF_PRESET_MODE_VALUE_TEMPLATE, + CONF_PRESET_MODES_LIST, CONF_RETAIN, + CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE, + CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC, + CONF_SWING_HORIZONTAL_MODE_LIST, + CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE, + CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC, + CONF_SWING_MODE_COMMAND_TEMPLATE, + CONF_SWING_MODE_COMMAND_TOPIC, + CONF_SWING_MODE_LIST, + CONF_SWING_MODE_STATE_TEMPLATE, + CONF_SWING_MODE_STATE_TOPIC, CONF_TEMP_COMMAND_TEMPLATE, CONF_TEMP_COMMAND_TOPIC, + CONF_TEMP_HIGH_COMMAND_TEMPLATE, + CONF_TEMP_HIGH_COMMAND_TOPIC, + CONF_TEMP_HIGH_STATE_TEMPLATE, + CONF_TEMP_HIGH_STATE_TOPIC, CONF_TEMP_INITIAL, + CONF_TEMP_LOW_COMMAND_TEMPLATE, + CONF_TEMP_LOW_COMMAND_TOPIC, + CONF_TEMP_LOW_STATE_TEMPLATE, + CONF_TEMP_LOW_STATE_TOPIC, CONF_TEMP_MAX, CONF_TEMP_MIN, CONF_TEMP_STATE_TEMPLATE, CONF_TEMP_STATE_TOPIC, + CONF_TEMP_STEP, + DEFAULT_CLIMATE_INITIAL_TEMPERATURE, DEFAULT_OPTIMISTIC, PAYLOAD_NONE, ) @@ -95,49 +131,6 @@ PARALLEL_UPDATES = 0 DEFAULT_NAME = "MQTT HVAC" -CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" -CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" -CONF_FAN_MODE_LIST = "fan_modes" -CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" -CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" - -CONF_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" -CONF_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" -CONF_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" -CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" -CONF_HUMIDITY_MAX = "max_humidity" -CONF_HUMIDITY_MIN = "min_humidity" - -CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" -CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" -CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" -CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" -CONF_PRESET_MODES_LIST = "preset_modes" - -CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template" -CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC = "swing_horizontal_mode_command_topic" -CONF_SWING_HORIZONTAL_MODE_LIST = "swing_horizontal_modes" -CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE = "swing_horizontal_mode_state_template" -CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC = "swing_horizontal_mode_state_topic" - -CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template" -CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" -CONF_SWING_MODE_LIST = "swing_modes" -CONF_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template" -CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic" - -CONF_TEMP_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template" -CONF_TEMP_HIGH_COMMAND_TOPIC = "temperature_high_command_topic" -CONF_TEMP_HIGH_STATE_TEMPLATE = "temperature_high_state_template" -CONF_TEMP_HIGH_STATE_TOPIC = "temperature_high_state_topic" -CONF_TEMP_LOW_COMMAND_TEMPLATE = "temperature_low_command_template" -CONF_TEMP_LOW_COMMAND_TOPIC = "temperature_low_command_topic" -CONF_TEMP_LOW_STATE_TEMPLATE = "temperature_low_state_template" -CONF_TEMP_LOW_STATE_TOPIC = "temperature_low_state_topic" -CONF_TEMP_STEP = "temp_step" - -DEFAULT_INITIAL_TEMPERATURE = 21.0 - MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( { climate.ATTR_CURRENT_HUMIDITY, @@ -299,8 +292,9 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_POWER_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_PRECISION): vol.In( - [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + vol.Optional(CONF_PRECISION): vol.All( + vol.Coerce(float), + vol.In([PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]), ), vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, @@ -577,7 +571,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): init_temp: float = config.get( CONF_TEMP_INITIAL, TemperatureConverter.convert( - DEFAULT_INITIAL_TEMPERATURE, + DEFAULT_CLIMATE_INITIAL_TEMPERATURE, UnitOfTemperature.CELSIUS, self.temperature_unit, ), diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 52f00c82c27..03f758dbdce 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -29,6 +29,13 @@ import yaml from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass +from homeassistant.components.climate import ( + DEFAULT_MAX_HUMIDITY, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, + DEFAULT_MIN_TEMP, + PRESET_NONE, +) from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.hassio import AddonError, AddonManager, AddonState @@ -80,6 +87,7 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, CONF_STATE_TEMPLATE, + CONF_TEMPERATURE_UNIT, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, @@ -89,8 +97,9 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, EntityCategory, + UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, async_get_hass, callback from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.hassio import is_hassio @@ -115,6 +124,7 @@ from homeassistant.helpers.selector import ( ) from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads +from homeassistant.util.unit_conversion import TemperatureConverter from .addon import get_addon_manager from .client import MqttClientSetup @@ -123,6 +133,8 @@ from .const import ( ATTR_QOS, ATTR_RETAIN, ATTR_TOPIC, + CONF_ACTION_TEMPLATE, + CONF_ACTION_TOPIC, CONF_AVAILABILITY_TEMPLATE, CONF_AVAILABILITY_TOPIC, CONF_BIRTH_MESSAGE, @@ -149,6 +161,10 @@ from .const import ( CONF_COMMAND_ON_TEMPLATE, CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_CURRENT_HUMIDITY_TEMPLATE, + CONF_CURRENT_HUMIDITY_TOPIC, + CONF_CURRENT_TEMP_TEMPLATE, + CONF_CURRENT_TEMP_TOPIC, CONF_DIRECTION_COMMAND_TEMPLATE, CONF_DIRECTION_COMMAND_TOPIC, CONF_DIRECTION_STATE_TOPIC, @@ -162,6 +178,11 @@ from .const import ( CONF_EFFECT_VALUE_TEMPLATE, CONF_ENTITY_PICTURE, CONF_EXPIRE_AFTER, + CONF_FAN_MODE_COMMAND_TEMPLATE, + CONF_FAN_MODE_COMMAND_TOPIC, + CONF_FAN_MODE_LIST, + CONF_FAN_MODE_STATE_TEMPLATE, + CONF_FAN_MODE_STATE_TOPIC, CONF_FLASH, CONF_FLASH_TIME_LONG, CONF_FLASH_TIME_SHORT, @@ -172,10 +193,21 @@ from .const import ( CONF_HS_COMMAND_TOPIC, CONF_HS_STATE_TOPIC, CONF_HS_VALUE_TEMPLATE, + CONF_HUMIDITY_COMMAND_TEMPLATE, + CONF_HUMIDITY_COMMAND_TOPIC, + CONF_HUMIDITY_MAX, + CONF_HUMIDITY_MIN, + CONF_HUMIDITY_STATE_TEMPLATE, + CONF_HUMIDITY_STATE_TOPIC, CONF_KEEPALIVE, CONF_LAST_RESET_VALUE_TEMPLATE, CONF_MAX_KELVIN, CONF_MIN_KELVIN, + CONF_MODE_COMMAND_TEMPLATE, + CONF_MODE_COMMAND_TOPIC, + CONF_MODE_LIST, + CONF_MODE_STATE_TEMPLATE, + CONF_MODE_STATE_TOPIC, CONF_OFF_DELAY, CONF_ON_COMMAND_TYPE, CONF_OPTIONS, @@ -200,6 +232,9 @@ from .const import ( CONF_PERCENTAGE_VALUE_TEMPLATE, CONF_POSITION_CLOSED, CONF_POSITION_OPEN, + CONF_POWER_COMMAND_TEMPLATE, + CONF_POWER_COMMAND_TOPIC, + CONF_PRECISION, CONF_PRESET_MODE_COMMAND_TEMPLATE, CONF_PRESET_MODE_COMMAND_TOPIC, CONF_PRESET_MODE_STATE_TOPIC, @@ -236,6 +271,32 @@ from .const import ( CONF_STATE_VALUE_TEMPLATE, CONF_SUGGESTED_DISPLAY_PRECISION, CONF_SUPPORTED_COLOR_MODES, + CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE, + CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC, + CONF_SWING_HORIZONTAL_MODE_LIST, + CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE, + CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC, + CONF_SWING_MODE_COMMAND_TEMPLATE, + CONF_SWING_MODE_COMMAND_TOPIC, + CONF_SWING_MODE_LIST, + CONF_SWING_MODE_STATE_TEMPLATE, + CONF_SWING_MODE_STATE_TOPIC, + CONF_TEMP_COMMAND_TEMPLATE, + CONF_TEMP_COMMAND_TOPIC, + CONF_TEMP_HIGH_COMMAND_TEMPLATE, + CONF_TEMP_HIGH_COMMAND_TOPIC, + CONF_TEMP_HIGH_STATE_TEMPLATE, + CONF_TEMP_HIGH_STATE_TOPIC, + CONF_TEMP_INITIAL, + CONF_TEMP_LOW_COMMAND_TEMPLATE, + CONF_TEMP_LOW_COMMAND_TOPIC, + CONF_TEMP_LOW_STATE_TEMPLATE, + CONF_TEMP_LOW_STATE_TOPIC, + CONF_TEMP_MAX, + CONF_TEMP_MIN, + CONF_TEMP_STATE_TEMPLATE, + CONF_TEMP_STATE_TOPIC, + CONF_TEMP_STEP, CONF_TILT_CLOSED_POSITION, CONF_TILT_COMMAND_TEMPLATE, CONF_TILT_COMMAND_TOPIC, @@ -260,6 +321,7 @@ from .const import ( CONFIG_ENTRY_MINOR_VERSION, CONFIG_ENTRY_VERSION, DEFAULT_BIRTH, + DEFAULT_CLIMATE_INITIAL_TEMPERATURE, DEFAULT_DISCOVERY, DEFAULT_ENCODING, DEFAULT_KEEPALIVE, @@ -392,6 +454,7 @@ KEY_UPLOAD_SELECTOR = FileSelector( SUBENTRY_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, Platform.FAN, Platform.LIGHT, @@ -493,6 +556,59 @@ TIMEOUT_SELECTOR = NumberSelector( NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0) ) +# Climate specific selectors +CLIMATE_MODE_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=["auto", "off", "cool", "heat", "dry", "fan_only"], + multiple=True, + translation_key="climate_modes", + ) +) + + +@callback +def temperature_selector(config: dict[str, Any]) -> Selector: + """Return a temperature selector with configured or system unit.""" + + return NumberSelector( + NumberSelectorConfig( + mode=NumberSelectorMode.BOX, + unit_of_measurement=cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]), + ) + ) + + +@callback +def temperature_step_selector(config: dict[str, Any]) -> Selector: + """Return a temperature step selector.""" + + return NumberSelector( + NumberSelectorConfig( + mode=NumberSelectorMode.BOX, + min=0.1, + max=10.0, + step=0.1, + unit_of_measurement=cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]), + ) + ) + + +TEMPERATURE_UNIT_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=[ + SelectOptionDict(value="C", label="°C"), + SelectOptionDict(value="F", label="°F"), + ], + mode=SelectSelectorMode.DROPDOWN, + ) +) +PRECISION_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=["1.0", "0.5", "0.1"], + mode=SelectSelectorMode.DROPDOWN, + ) +) + # Cover specific selectors POSITION_SELECTOR = NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX)) @@ -567,10 +683,91 @@ SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector( EXCLUDE_FROM_CONFIG_IF_NONE = {CONF_ENTITY_CATEGORY} +# Target temperature feature selector @callback -def validate_cover_platform_config( - config: dict[str, Any], -) -> dict[str, str]: +def configured_target_temperature_feature(config: dict[str, Any]) -> str: + """Calculate current target temperature feature from config.""" + if ( + config == {CONF_PLATFORM: Platform.CLIMATE.value} + or CONF_TEMP_COMMAND_TOPIC in config + ): + # default to single on initial set + return "single" + if CONF_TEMP_HIGH_COMMAND_TOPIC in config: + return "high_low" + return "none" + + +TARGET_TEMPERATURE_FEATURE_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=["single", "high_low", "none"], + mode=SelectSelectorMode.DROPDOWN, + translation_key="target_temperature_feature", + ) +) +HUMIDITY_SELECTOR = vol.All( + NumberSelector( + NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=100, step=1) + ), + vol.Coerce(int), +) + + +@callback +def temperature_default_from_celsius_to_system_default( + value: float, +) -> Callable[[dict[str, Any]], int]: + """Return temperature in Celsius in system default unit.""" + + def _default(config: dict[str, Any]) -> int: + return round( + TemperatureConverter.convert( + value, + UnitOfTemperature.CELSIUS, + cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]), + ) + ) + + return _default + + +@callback +def default_precision(config: dict[str, Any]) -> str: + """Return the thermostat precision for system default unit.""" + + return str( + config.get( + CONF_PRECISION, + 0.1 + if cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]) + is UnitOfTemperature.CELSIUS + else 1.0, + ) + ) + + +@callback +def validate_climate_platform_config(config: dict[str, Any]) -> dict[str, str]: + """Validate the climate platform options.""" + errors: dict[str, str] = {} + if ( + CONF_PRESET_MODES_LIST in config + and PRESET_NONE in config[CONF_PRESET_MODES_LIST] + ): + errors["climate_preset_mode_settings"] = "preset_mode_none_not_allowed" + if ( + CONF_HUMIDITY_MIN in config + and config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX] + ): + errors["target_humidity_settings"] = "max_below_min_humidity" + if CONF_TEMP_MIN in config and config[CONF_TEMP_MIN] >= config[CONF_TEMP_MAX]: + errors["target_temperature_settings"] = "max_below_min_temperature" + + return errors + + +@callback +def validate_cover_platform_config(config: dict[str, Any]) -> dict[str, str]: """Validate the cover platform options.""" errors: dict[str, str] = {} @@ -680,6 +877,14 @@ def validate_sensor_platform_config( return errors +@callback +def no_empty_list(value: list[Any]) -> list[Any]: + """Validate a selector returns at least one item.""" + if not value: + raise vol.Invalid("empty_list_not_allowed") + return value + + @callback def validate(validator: Callable[[Any], Any]) -> Callable[[Any], Any]: """Run validator, then return the unmodified input.""" @@ -695,13 +900,13 @@ def validate(validator: Callable[[Any], Any]) -> Callable[[Any], Any]: class PlatformField: """Stores a platform config field schema, required flag and validator.""" - selector: Selector[Any] | Callable[..., Selector[Any]] + selector: Selector[Any] | Callable[[dict[str, Any]], Selector[Any]] required: bool - validator: Callable[..., Any] | None = None + validator: Callable[[Any], Any] | None = None error: str | None = None - default: ( - str | int | bool | None | Callable[[dict[str, Any]], Any] | vol.Undefined - ) = vol.UNDEFINED + default: Any | None | Callable[[dict[str, Any]], Any] | vol.Undefined = ( + vol.UNDEFINED + ) is_schema_default: bool = False exclude_from_reconfig: bool = False exclude_from_config: bool = False @@ -790,6 +995,78 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { required=False, ), }, + Platform.CLIMATE.value: { + CONF_TEMPERATURE_UNIT: PlatformField( + selector=TEMPERATURE_UNIT_SELECTOR, + validator=validate(cv.temperature_unit), + required=True, + exclude_from_reconfig=True, + default=lambda _: "C" + if async_get_hass().config.units.temperature_unit + is UnitOfTemperature.CELSIUS + else "F", + ), + "climate_feature_action": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_ACTION_TOPIC)), + ), + "climate_feature_target_temperature": PlatformField( + selector=TARGET_TEMPERATURE_FEATURE_SELECTOR, + required=False, + exclude_from_config=True, + default=configured_target_temperature_feature, + ), + "climate_feature_current_temperature": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_CURRENT_TEMP_TOPIC)), + ), + "climate_feature_target_humidity": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_HUMIDITY_COMMAND_TOPIC)), + ), + "climate_feature_current_humidity": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_HUMIDITY_STATE_TOPIC)), + ), + "climate_feature_preset_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_PRESET_MODES_LIST)), + ), + "climate_feature_fan_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_FAN_MODE_LIST)), + ), + "climate_feature_swing_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_SWING_MODE_LIST)), + ), + "climate_feature_swing_horizontal_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_SWING_HORIZONTAL_MODE_LIST)), + ), + "climate_feature_power": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_POWER_COMMAND_TOPIC)), + ), + }, Platform.COVER.value: { CONF_DEVICE_CLASS: PlatformField( selector=COVER_DEVICE_CLASS_SELECTOR, @@ -929,6 +1206,496 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { ), CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, + Platform.CLIMATE.value: { + # operation mode settings + CONF_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_MODE_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_MODE_LIST: PlatformField( + selector=CLIMATE_MODE_SELECTOR, + required=True, + default=[], + validator=validate(no_empty_list), + error="empty_list_not_allowed", + ), + CONF_RETAIN: PlatformField( + selector=BOOLEAN_SELECTOR, required=False, validator=validate(bool) + ), + CONF_OPTIMISTIC: PlatformField( + selector=BOOLEAN_SELECTOR, required=False, validator=validate(bool) + ), + # current action settings + CONF_ACTION_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_action_settings", + conditions=({"climate_feature_action": True},), + ), + CONF_ACTION_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_action_settings", + conditions=({"climate_feature_action": True},), + ), + # target temperature settings + CONF_TEMP_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "single"},), + ), + CONF_TEMP_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "single"},), + ), + CONF_TEMP_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "single"},), + ), + CONF_TEMP_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "single"},), + ), + CONF_TEMP_LOW_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_LOW_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_LOW_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_LOW_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_HIGH_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_HIGH_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_HIGH_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_HIGH_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_MIN: PlatformField( + selector=temperature_selector, + custom_filtering=True, + required=True, + default=temperature_default_from_celsius_to_system_default( + DEFAULT_MIN_TEMP + ), + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + CONF_TEMP_MAX: PlatformField( + selector=temperature_selector, + custom_filtering=True, + required=True, + default=temperature_default_from_celsius_to_system_default( + DEFAULT_MAX_TEMP + ), + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + CONF_PRECISION: PlatformField( + selector=PRECISION_SELECTOR, + required=False, + default=default_precision, + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + CONF_TEMP_STEP: PlatformField( + selector=temperature_step_selector, + custom_filtering=True, + required=False, + default=1.0, + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + CONF_TEMP_INITIAL: PlatformField( + selector=temperature_selector, + custom_filtering=True, + required=False, + default=temperature_default_from_celsius_to_system_default( + DEFAULT_CLIMATE_INITIAL_TEMPERATURE + ), + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + # current temperature settings + CONF_CURRENT_TEMP_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="current_temperature_settings", + conditions=({"climate_feature_current_temperature": True},), + ), + CONF_CURRENT_TEMP_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="current_temperature_settings", + conditions=({"climate_feature_current_temperature": True},), + ), + # target humidity settings + CONF_HUMIDITY_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_MIN: PlatformField( + selector=HUMIDITY_SELECTOR, + required=True, + default=DEFAULT_MIN_HUMIDITY, + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_MAX: PlatformField( + selector=HUMIDITY_SELECTOR, + required=True, + default=DEFAULT_MAX_HUMIDITY, + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + # current humidity settings + CONF_CURRENT_HUMIDITY_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="current_humidity_settings", + conditions=({"climate_feature_current_humidity": True},), + ), + CONF_CURRENT_HUMIDITY_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="current_humidity_settings", + conditions=({"climate_feature_current_humidity": True},), + ), + # power on/off support + CONF_POWER_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_power_settings", + conditions=({"climate_feature_power": True},), + ), + CONF_POWER_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_power_settings", + conditions=({"climate_feature_power": True},), + ), + CONF_PAYLOAD_OFF: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_OFF, + section="climate_power_settings", + conditions=({"climate_feature_power": True},), + ), + CONF_PAYLOAD_ON: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ON, + section="climate_power_settings", + conditions=({"climate_feature_power": True},), + ), + # preset mode settings + CONF_PRESET_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + CONF_PRESET_MODES_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + validator=validate(no_empty_list), + error="empty_list_not_allowed", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + # fan mode settings + CONF_FAN_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + CONF_FAN_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + CONF_FAN_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + CONF_FAN_MODE_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + CONF_FAN_MODE_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + validator=validate(no_empty_list), + error="empty_list_not_allowed", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + # swing mode settings + CONF_SWING_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + CONF_SWING_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + CONF_SWING_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + CONF_SWING_MODE_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + CONF_SWING_MODE_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + validator=validate(no_empty_list), + error="empty_list_not_allowed", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + # swing horizontal mode settings + CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + CONF_SWING_HORIZONTAL_MODE_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + validator=validate(no_empty_list), + error="empty_list_not_allowed", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + }, Platform.COVER.value: { CONF_COMMAND_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -1904,6 +2671,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ ] = { Platform.BINARY_SENSOR.value: None, Platform.BUTTON.value: None, + Platform.CLIMATE.value: validate_climate_platform_config, Platform.COVER.value: validate_cover_platform_config, Platform.FAN.value: validate_fan_platform_config, Platform.LIGHT.value: validate_light_platform_config, @@ -2097,15 +2865,15 @@ def data_schema_from_fields( no_reconfig_options: set[Any] = set() for schema_section in sections: data_schema_element = { - vol.Required(field_name, default=field_details.default) + vol.Required(field_name, default=get_default(field_details)) if field_details.required else vol.Optional( field_name, default=get_default(field_details) if field_details.default is not None else vol.UNDEFINED, - ): field_details.selector(component_data_with_user_input) # type: ignore[operator] - if field_details.custom_filtering + ): field_details.selector(component_data_with_user_input or {}) + if callable(field_details.selector) and field_details.custom_filtering else field_details.selector for field_name, field_details in data_schema_fields.items() if not field_details.is_schema_default @@ -2127,12 +2895,20 @@ def data_schema_from_fields( if not data_schema_element: # Do not show empty sections continue + # Collapse if values are changed or required fields need to be set collapsed = ( not any( (default := data_schema_fields[str(option)].default) is vol.UNDEFINED - or component_data_with_user_input[str(option)] != default + or ( + str(option) in component_data_with_user_input + and component_data_with_user_input[str(option)] != default + ) for option in data_element_options if option in component_data_with_user_input + or ( + str(option) in data_schema_fields + and data_schema_fields[str(option)].required + ) ) if component_data_with_user_input is not None else True diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index c60aa674b1b..1dfdb8dac53 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -26,7 +26,6 @@ CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_AVAILABILITY = "availability" - CONF_AVAILABILITY_MODE = "availability_mode" CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_AVAILABILITY_TOPIC = "availability_topic" @@ -53,7 +52,6 @@ CONF_WS_HEADERS = "ws_headers" CONF_WILL_MESSAGE = "will_message" CONF_PAYLOAD_RESET = "payload_reset" CONF_SUPPORTED_FEATURES = "supported_features" - CONF_ACTION_TEMPLATE = "action_template" CONF_ACTION_TOPIC = "action_topic" CONF_BLUE_TEMPLATE = "blue_template" @@ -91,6 +89,11 @@ CONF_EFFECT_TEMPLATE = "effect_template" CONF_EFFECT_VALUE_TEMPLATE = "effect_value_template" CONF_ENTITY_PICTURE = "entity_picture" CONF_EXPIRE_AFTER = "expire_after" +CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" +CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" +CONF_FAN_MODE_LIST = "fan_modes" +CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" +CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" CONF_FLASH = "flash" CONF_FLASH_TIME_LONG = "flash_time_long" CONF_FLASH_TIME_SHORT = "flash_time_short" @@ -101,6 +104,12 @@ CONF_HS_COMMAND_TEMPLATE = "hs_command_template" CONF_HS_COMMAND_TOPIC = "hs_command_topic" CONF_HS_STATE_TOPIC = "hs_state_topic" CONF_HS_VALUE_TEMPLATE = "hs_value_template" +CONF_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" +CONF_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" +CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" +CONF_HUMIDITY_MAX = "max_humidity" +CONF_HUMIDITY_MIN = "min_humidity" CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template" CONF_MAX_KELVIN = "max_kelvin" CONF_MAX_MIREDS = "max_mireds" @@ -166,13 +175,32 @@ CONF_STATE_OPENING = "state_opening" CONF_STATE_STOPPED = "state_stopped" CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision" CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" +CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template" +CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC = "swing_horizontal_mode_command_topic" +CONF_SWING_HORIZONTAL_MODE_LIST = "swing_horizontal_modes" +CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE = "swing_horizontal_mode_state_template" +CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC = "swing_horizontal_mode_state_topic" +CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template" +CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" +CONF_SWING_MODE_LIST = "swing_modes" +CONF_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template" +CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic" CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template" CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic" -CONF_TEMP_STATE_TEMPLATE = "temperature_state_template" -CONF_TEMP_STATE_TOPIC = "temperature_state_topic" +CONF_TEMP_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template" +CONF_TEMP_HIGH_COMMAND_TOPIC = "temperature_high_command_topic" +CONF_TEMP_HIGH_STATE_TEMPLATE = "temperature_high_state_template" +CONF_TEMP_HIGH_STATE_TOPIC = "temperature_high_state_topic" CONF_TEMP_INITIAL = "initial" +CONF_TEMP_LOW_COMMAND_TEMPLATE = "temperature_low_command_template" +CONF_TEMP_LOW_COMMAND_TOPIC = "temperature_low_command_topic" +CONF_TEMP_LOW_STATE_TEMPLATE = "temperature_low_state_template" +CONF_TEMP_LOW_STATE_TOPIC = "temperature_low_state_topic" CONF_TEMP_MAX = "max_temp" CONF_TEMP_MIN = "min_temp" +CONF_TEMP_STATE_TEMPLATE = "temperature_state_template" +CONF_TEMP_STATE_TOPIC = "temperature_state_topic" +CONF_TEMP_STEP = "temp_step" CONF_TILT_COMMAND_TEMPLATE = "tilt_command_template" CONF_TILT_COMMAND_TOPIC = "tilt_command_topic" CONF_TILT_STATUS_TOPIC = "tilt_status_topic" @@ -213,6 +241,7 @@ CONF_SUPPORT_URL = "support_url" DEFAULT_BRIGHTNESS = False DEFAULT_BRIGHTNESS_SCALE = 255 +DEFAULT_CLIMATE_INITIAL_TEMPERATURE = 21.0 DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = True diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 92900d8292d..22fb85780b0 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -239,6 +239,16 @@ "title": "Configure MQTT device \"{mqtt_device}\"", "description": "Please configure specific details for {platform} entity \"{entity}\":", "data": { + "climate_feature_action": "Current action support", + "climate_feature_current_humidity": "Current humidity support", + "climate_feature_current_temperature": "Current temperature support", + "climate_feature_fan_modes": "Fan mode support", + "climate_feature_power": "Power on/off support", + "climate_feature_preset_modes": "[%key:component::mqtt::config_subentries::device::step::entity_platform_config::data::fan_feature_preset_modes%]", + "climate_feature_swing_horizontal_modes": "Horizontal swing mode support", + "climate_feature_swing_modes": "Swing mode support", + "climate_feature_target_temperature": "Target temperature support", + "climate_feature_target_humidity": "Target humidity support", "device_class": "Device class", "entity_category": "Entity category", "fan_feature_speed": "Speed support", @@ -249,9 +259,20 @@ "schema": "Schema", "state_class": "State class", "suggested_display_precision": "Suggested display precision", + "temperature_unit": "Temperature unit", "unit_of_measurement": "Unit of measurement" }, "data_description": { + "climate_feature_action": "The climate supports reporting the current action.", + "climate_feature_current_humidity": "The climate supports reporting the current humidity.", + "climate_feature_current_temperature": "The climate supports reporting the current temperature.", + "climate_feature_fan_modes": "The climate supports fan modes.", + "climate_feature_power": "The climate supports the power \"on\" and \"off\" commands.", + "climate_feature_preset_modes": "The climate supports preset modes.", + "climate_feature_swing_horizontal_modes": "The climate supports horizontal swing modes.", + "climate_feature_swing_modes": "The climate supports swing modes.", + "climate_feature_target_temperature": "The climate supports setting the target temperature.", + "climate_feature_target_humidity": "The climate supports setting the target humidity.", "device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)", "entity_category": "Allows marking an entity as device configuration or diagnostics. An entity with a category will not be exposed to cloud, Alexa, or Google Assistant components, nor included in indirect action calls to devices or areas. Sensor entities cannot be assigned a device configuration class. [Learn more.](https://developers.home-assistant.io/docs/core/entity/#registry-properties)", "fan_feature_speed": "The fan supports multiple speeds.", @@ -262,6 +283,7 @@ "schema": "The schema to use. [Learn more.]({url}#comparison-of-light-mqtt-schemas)", "state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)", "suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)", + "temperature_unit": "This determines the native unit of measurement the MQTT climate device works with.", "unit_of_measurement": "Defines the unit of measurement of the sensor, if any." }, "sections": { @@ -290,6 +312,11 @@ "force_update": "Force update", "green_template": "Green template", "last_reset_value_template": "Last reset value template", + "modes": "Supported operation modes", + "mode_command_topic": "Operation mode command topic", + "mode_command_template": "Operation mode command template", + "mode_state_topic": "Operation mode state topic", + "mode_state_template": "Operation mode value template", "on_command_type": "ON command type", "optimistic": "Optimistic", "payload_off": "Payload \"off\"", @@ -317,6 +344,11 @@ "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", "green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.", "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", + "modes": "A list of supported operation modes. [Learn more.]({url}#modes)", + "mode_command_topic": "The MQTT topic to publish commands to change the climate operation mode. [Learn more.]({url}#mode_command_topic)", + "mode_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the operation mode to be sent to the operation mode command topic. [Learn more.]({url}#mode_command_template)", + "mode_state_topic": "The MQTT topic subscribed to receive operation mode state messages. [Learn more.]({url}#mode_state_topic)", + "mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the operation mode state. [Learn more.]({url}#mode_state_template)", "on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.", "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", "payload_off": "The payload that represents the \"off\" state.", @@ -356,6 +388,100 @@ "transition": "Enable the transition feature for this light" } }, + "climate_action_settings": { + "name": "Current action settings", + "data": { + "action_template": "Action template", + "action_topic": "Action topic" + }, + "data_description": { + "action_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the action topic with.", + "action_topic": "The MQTT topic to subscribe for changes of the current action. If this is set, the climate graph uses the value received as data source. A \"None\" payload resets the current action state. An empty payload is ignored. Valid action values are: \"off\", \"heating\", \"cooling\", \"drying\", \"idle\" and \"fan\". [Learn more.]({url}#action_topic)" + } + }, + "climate_fan_mode_settings": { + "name": "Fan mode settings", + "data": { + "fan_modes": "Fan modes", + "fan_mode_command_topic": "Fan mode command topic", + "fan_mode_command_template": "Fan mode command template", + "fan_mode_state_topic": "Fan mode state topic", + "fan_mode_state_template": "Fan mode state template" + }, + "data_description": { + "fan_modes": "List of fan modes this climate is capable of running at. Common fan modes that offer translations are `off`, `on`, `auto`, `low`, `medium`, `high`, `middle`, `focus` and `diffuse`.", + "fan_mode_command_topic": "The MQTT topic to publish commands to change the climate fan mode. [Learn more.]({url}#fan_mode_command_topic)", + "fan_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the fan mode command topic.", + "fan_mode_state_topic": "The MQTT topic subscribed to receive the climate fan mode. [Learn more.]({url}#fan_mode_state_topic)", + "fan_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate fan mode value." + } + }, + "climate_power_settings": { + "name": "Power settings", + "data": { + "payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data::payload_off%]", + "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data::payload_on%]", + "power_command_template": "Power command template", + "power_command_topic": "Power command topic" + }, + "data_description": { + "payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]", + "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]", + "power_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".", + "power_command_topic": "The MQTT topic to publish commands to change the climate power state. Sends the payload configured with payload \"on\" or payload \"off\". [Learn more.]({url}#power_command_topic)" + } + }, + "climate_preset_mode_settings": { + "name": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::name%]", + "data": { + "preset_mode_command_template": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_mode_command_template%]", + "preset_mode_command_topic": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_mode_command_topic%]", + "preset_mode_value_template": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_mode_value_template%]", + "preset_mode_state_topic": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_mode_state_topic%]", + "preset_modes": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_modes%]" + }, + "data_description": { + "preset_mode_command_template": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data_description::preset_mode_command_template%]", + "preset_mode_command_topic": "The MQTT topic to publish commands to change the climate preset mode. [Learn more.]({url}#preset_mode_command_topic)", + "preset_mode_value_template": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data_description::preset_mode_value_template%]", + "preset_mode_state_topic": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data_description::preset_mode_state_topic%]", + "preset_modes": "List of preset modes this climate is capable of running at. Common preset modes that offer translations are `none`, `away`, `eco`, `boost`, `comfort`, `home`, `sleep` and `activity`." + } + }, + "climate_swing_horizontal_mode_settings": { + "name": "Horizontal swing mode settings", + "data": { + "swing_horizontal_modes": "Horizontal swing modes", + "swing_horizontal_mode_command_topic": "Horizontal swing mode command topic", + "swing_horizontal_mode_command_template": "Horizontal swing mode command template", + "swing_horizontal_mode_state_topic": "Horizontal swing mode state topic", + "swing_horizontal_mode_state_template": "Horizontal swing mode state template" + }, + "data_description": { + "swing_horizontal_modes": "List of horizontal swing modes this climate is capable of running at. Common horizontal swing modes that offer translations are `off` and `on`.", + "swing_horizontal_mode_command_topic": "The MQTT topic to publish commands to change the climate horizontal swing mode. [Learn more.]({url}#swing_horizontal_mode_command_topic)", + "swing_horizontal_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the horizontal swing mode command topic.", + "swing_horizontal_mode_state_topic": "The MQTT topic subscribed to receive the climate horizontal swing mode. [Learn more.]({url}#swing_horizontal_mode_state_topic)", + "swing_horizontal_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate horizontal swing mode value." + } + }, + "climate_swing_mode_settings": { + "name": "Swing mode settings", + "data": { + "swing_modes": "Swing modes", + "swing_mode_command_topic": "Swing mode command topic", + "swing_mode_command_template": "Swing mode command template", + "swing_mode_state_topic": "Swing mode state topic", + "swing_mode_state_template": "Swing mode state template" + }, + "data_description": { + "swing_modes": "List of swing modes this climate is capable of running at. Common swing modes that offer translations are `off`, `on`, `vertical`, `horizontal` and `both`.", + "swing_mode_command_topic": "The MQTT topic to publish commands to change the climate swing mode. [Learn more.]({url}#swing_mode_command_topic)", + "swing_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the swing mode command topic.", + "swing_mode_state_topic": "The MQTT topic subscribed to receive the climate swing mode. [Learn more.]({url}#swing_mode_state_topic)", + "swing_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate swing mode value." + } + }, "cover_payload_settings": { "name": "Payload settings", "data": { @@ -425,6 +551,28 @@ "tilt_optimistic": "Flag that defines if tilt works in optimistic mode. If tilt status topic is not defined, tilt works in optimistic mode by default. [Learn more.]({url}#tilt_optimistic)" } }, + "current_humidity_settings": { + "name": "Current humidity settings", + "data": { + "current_humidity_template": "Current humidity template", + "current_humidity_topic": "Current humidity topic" + }, + "data_description": { + "current_humidity_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the current humidity value. [Learn more.]({url}#current_humidity_template)", + "current_humidity_topic": "The MQTT topic subscribed to receive current humidity update values. [Learn more.]({url}#current_humidity_topic)" + } + }, + "current_temperature_settings": { + "name": "Current temperature settings", + "data": { + "current_temperature_template": "Current temperature template", + "current_temperature_topic": "Current temperature topic" + }, + "data_description": { + "current_temperature_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the current temperature value. [Learn more.]({url}#current_temperature_template)", + "current_temperature_topic": "The MQTT topic subscribed to receive current temperature update values. [Learn more.]({url}#current_temperature_topic)" + } + }, "light_brightness_settings": { "name": "Brightness settings", "data": { @@ -648,6 +796,66 @@ "xy_state_topic": "The MQTT topic subscribed to receive XY state updates. The expected payload is the X and Y color values separated by commas, for example, `0.675,0.322`. [Learn more.]({url}#xy_state_topic)", "xy_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the XY value." } + }, + "target_humidity_settings": { + "name": "Target humidity settings", + "data": { + "max_humidity": "Maximum humidity", + "min_humidity": "Minimum humidity", + "target_humidity_command_template": "Humidity command template", + "target_humidity_command_topic": "Humidity command topic", + "target_humidity_state_template": "Humidity state template", + "target_humidity_state_topic": "Humidity state topic" + }, + "data_description": { + "max_humidity": "The maximum target humidity that can be set.", + "min_humidity": "The minimum target humidity that can be set.", + "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the humidity command topic.", + "target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)", + "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the humidity state topic with.", + "target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)" + } + }, + "target_temperature_settings": { + "name": "Target temperature settings", + "data": { + "initial": "Initial temperature", + "max_temp": "Maximum temperature", + "min_temp": "Minimum temperature", + "precision": "Precision", + "temp_step": "Temperature step", + "temperature_command_template": "Temperature command template", + "temperature_command_topic": "Temperature command topic", + "temperature_high_command_template": "Upper temperature command template", + "temperature_high_command_topic": "Upper temperature command topic", + "temperature_low_command_template": "Lower temperature command template", + "temperature_low_command_topic": "Lower temperature command topic", + "temperature_state_template": "Temperature state template", + "temperature_state_topic": "Temperature state topic", + "temperature_high_state_template": "Upper temperature state template", + "temperature_high_state_topic": "Upper temperature state topic", + "temperature_low_state_template": "Lower temperature state template", + "temperature_low_state_topic": "Lower temperature state topic" + }, + "data_description": { + "initial": "The climate initalizes with this target temperature.", + "max_temp": "The maximum target temperature that can be set.", + "min_temp": "The minimum target temperature that can be set.", + "precision": "The precision in degrees the thermostat is working at.", + "temp_step": "The target temperature step in degrees Celsius or Fahrenheit.", + "temperature_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the temperature command topic.", + "temperature_command_topic": "The MQTT topic to publish commands to change the climate target temperature. [Learn more.]({url}#temperature_command_topic)", + "temperature_high_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the upper temperature command topic.", + "temperature_high_command_topic": "The MQTT topic to publish commands to change the climate upper target temperature. [Learn more.]({url}#temperature_high_command_topic)", + "temperature_low_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the lower temperature command topic.", + "temperature_low_command_topic": "The MQTT topic to publish commands to change the climate lower target temperature. [Learn more.]({url}#temperature_low_command_topic)", + "temperature_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the temperature state topic with.", + "temperature_state_topic": "The MQTT topic to subscribe for changes of the target temperature. [Learn more.]({url}#temperature_state_topic)", + "temperature_high_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the upper temperature state topic with.", + "temperature_high_state_topic": "The MQTT topic to subscribe for changes of the upper target temperature. [Learn more.]({url}#temperature_high_state_topic)", + "temperature_low_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the lower temperature state topic with.", + "temperature_low_state_topic": "The MQTT topic to subscribe for changes of the lower target temperature. [Learn more.]({url}#temperature_low_state_topic)" + } } } }, @@ -695,6 +903,7 @@ "cover_tilt_command_template_must_be_used_with_tilt_command_topic": "The tilt command template must be used with the tilt command topic", "cover_tilt_status_template_must_be_used_with_tilt_status_topic": "The tilt value template must be used with the tilt status topic", "cover_value_template_must_be_used_with_state_topic": "The value template must be used with the state topic option", + "empty_list_not_allowed": "Empty list is not allowed. Add at least one item", "fan_speed_range_max_must_be_greater_than_speed_range_min": "Speed range max must be greater than speed range min", "fan_preset_mode_reset_in_preset_modes_list": "Payload \"reset preset mode\" is not a valid as a preset mode", "invalid_input": "Invalid value", @@ -705,10 +914,13 @@ "invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list", "invalid_url": "Invalid URL", "last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only", + "max_below_min_humidity": "Max humidity value should be greater than min humidity value", "max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value", + "max_below_min_temperature": "Max temperature value should be greater than min temperature value", "options_not_allowed_with_state_class_or_uom": "The 'Options' setting is not allowed when state class or unit of measurement are used", "options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class. If you continue, the existing options will be reset", "options_with_enum_device_class": "Configure options for the enumeration sensor", + "preset_mode_none_not_allowed": "Preset \"none\" is not a valid preset mode", "uom_required_for_device_class": "The selected device class requires a unit" } } @@ -826,6 +1038,17 @@ } }, "selector": { + "climate_modes": { + "options": { + "off": "[%key:common::state::off%]", + "auto": "[%key:common::state::auto%]", + "heat": "[%key:component::climate::entity_component::_::state::heat%]", + "cool": "[%key:component::climate::entity_component::_::state::cool%]", + "heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]", + "dry": "[%key:component::climate::entity_component::_::state::dry%]", + "fan_only": "[%key:component::climate::entity_component::_::state::fan_only%]" + } + }, "device_class_binary_sensor": { "options": { "battery": "[%key:component::binary_sensor::entity_component::battery::name%]", @@ -969,6 +1192,7 @@ "options": { "binary_sensor": "[%key:component::binary_sensor::title%]", "button": "[%key:component::button::title%]", + "climate": "[%key:component::climate::title%]", "cover": "[%key:component::cover::title%]", "fan": "[%key:component::fan::title%]", "light": "[%key:component::light::title%]", @@ -1004,6 +1228,13 @@ "rgbww": "[%key:component::light::entity_component::_::state_attributes::color_mode::state::rgbww%]", "white": "[%key:component::light::entity_component::_::state_attributes::color_mode::state::white%]" } + }, + "target_temperature_feature": { + "options": { + "single": "Single target temperature", + "high_low": "Upper/lower target temperature", + "none": "No target temperature" + } } }, "services": { diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 3e87925c1cd..15e203eab06 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -94,6 +94,117 @@ MOCK_SUBENTRY_BUTTON_COMPONENT = { "entity_picture": "https://example.com/365d05e6607c4dfb8ae915cff71a954b", }, } +MOCK_SUBENTRY_CLIMATE_COMPONENT = { + "b085c09efba7ec76acd94e2e0f851386": { + "platform": "climate", + "name": "Cooler", + "entity_category": None, + "entity_picture": "https://example.com/b085c09efba7ec76acd94e2e0f851386", + "temperature_unit": "C", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + # single target temperature + "temperature_command_topic": "temperature-command-topic", + "temperature_command_template": "{{ value }}", + "temperature_state_topic": "temperature-state-topic", + "temperature_state_template": "{{ value_json.temperature }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + # power settings + "power_command_topic": "power-command-topic", + "power_command_template": "{{ value }}", + "payload_on": "ON", + "payload_off": "OFF", + # current action settings + "action_topic": "action-topic", + "action_template": "{{ value_json.current_action }}", + # target humidity + "target_humidity_command_topic": "target-humidity-command-topic", + "target_humidity_command_template": "{{ value }}", + "target_humidity_state_topic": "target-humidity-state-topic", + "target_humidity_state_template": "{{ value_json.target_humidity }}", + "min_humidity": 20, + "max_humidity": 80, + # current temperature + "current_temperature_topic": "current-temperature-topic", + "current_temperature_template": "{{ value_json.temperature }}", + # current humidity + "current_humidity_topic": "current-humidity-topic", + "current_humidity_template": "{{ value_json.humidity }}", + # preset mode + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_command_template": "{{ value }}", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "preset_modes": ["auto", "eco"], + # fan mode + "fan_mode_command_topic": "fan-mode-command-topic", + "fan_mode_command_template": "{{ value }}", + "fan_mode_state_topic": "fan-mode-state-topic", + "fan_mode_state_template": "{{ value_json.fan_mode }}", + "fan_modes": ["off", "low", "medium", "high"], + # swing mode + "swing_mode_command_topic": "swing-mode-command-topic", + "swing_mode_command_template": "{{ value }}", + "swing_mode_state_topic": "swing-mode-state-topic", + "swing_mode_state_template": "{{ value_json.swing_mode }}", + "swing_modes": ["off", "on"], + # swing horizontal mode + "swing_horizontal_mode_command_topic": "swing-horizontal-mode-command-topic", + "swing_horizontal_mode_command_template": "{{ value }}", + "swing_horizontal_mode_state_topic": "swing-horizontal-mode-state-topic", + "swing_horizontal_mode_state_template": "{{ value_json.swing_horizontal_mode }}", + "swing_horizontal_modes": ["off", "on"], + }, +} +MOCK_SUBENTRY_CLIMATE_HIGH_LOW_COMPONENT = { + "b085c09efba7ec76acd94e2e0f851387": { + "platform": "climate", + "name": "Cooler", + "entity_category": None, + "entity_picture": "https://example.com/b085c09efba7ec76acd94e2e0f851387", + "temperature_unit": "C", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + # high/low target temperature + "temperature_low_command_topic": "temperature-low-command-topic", + "temperature_low_command_template": "{{ value }}", + "temperature_low_state_topic": "temperature-low-state-topic", + "temperature_low_state_template": "{{ value_json.temperature_low }}", + "temperature_high_command_topic": "temperature-high-command-topic", + "temperature_high_command_template": "{{ value }}", + "temperature_high_state_topic": "temperature-high-state-topic", + "temperature_high_state_template": "{{ value_json.temperature_high }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, +} +MOCK_SUBENTRY_CLIMATE_NO_TARGET_TEMP_COMPONENT = { + "b085c09efba7ec76acd94e2e0f851388": { + "platform": "climate", + "name": "Cooler", + "entity_category": None, + "entity_picture": "https://example.com/b085c09efba7ec76acd94e2e0f851388", + "temperature_unit": "C", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + }, +} MOCK_SUBENTRY_COVER_COMPONENT = { "b37acf667fa04c688ad7dfb27de2178b": { "platform": "cover", @@ -312,6 +423,18 @@ MOCK_BUTTON_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, "components": MOCK_SUBENTRY_BUTTON_COMPONENT, } +MOCK_CLIMATE_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_CLIMATE_COMPONENT, +} +MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, + "components": MOCK_SUBENTRY_CLIMATE_HIGH_LOW_COMPONENT, +} +MOCK_CLIMATE_NO_TARGET_TEMP_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, + "components": MOCK_SUBENTRY_CLIMATE_NO_TARGET_TEMP_COMPONENT, +} MOCK_COVER_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_COVER_COMPONENT, diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 568fb7ea39d..333febe8844 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -29,10 +29,12 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.components.mqtt.climate import ( - DEFAULT_INITIAL_TEMPERATURE, MQTT_CLIMATE_ATTRIBUTES_BLOCKED, VALUE_TEMPLATE_KEYS, ) +from homeassistant.components.mqtt.const import ( + DEFAULT_CLIMATE_INITIAL_TEMPERATURE as DEFAULT_INITIAL_TEMPERATURE, +) from homeassistant.const import ATTR_TEMPERATURE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index ce0a0c44a79..ff1f954bace 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -35,6 +35,9 @@ from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .common import ( MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_BUTTON_SUBENTRY_DATA_SINGLE, + MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE, + MOCK_CLIMATE_NO_TARGET_TEMP_SUBENTRY_DATA_SINGLE, + MOCK_CLIMATE_SUBENTRY_DATA_SINGLE, MOCK_COVER_SUBENTRY_DATA_SINGLE, MOCK_FAN_SUBENTRY_DATA_SINGLE, MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE, @@ -2700,6 +2703,224 @@ async def test_migrate_of_incompatible_config_entry( ), "Milk notifier Restart", ), + ( + MOCK_CLIMATE_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Cooler"}, + { + "temperature_unit": "C", + "climate_feature_action": True, + "climate_feature_current_humidity": True, + "climate_feature_current_temperature": True, + "climate_feature_power": True, + "climate_feature_preset_modes": True, + "climate_feature_fan_modes": True, + "climate_feature_swing_horizontal_modes": True, + "climate_feature_swing_modes": True, + "climate_feature_target_temperature": "single", + "climate_feature_target_humidity": True, + }, + (), + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + # single target temperature + "target_temperature_settings": { + "temperature_command_topic": "temperature-command-topic", + "temperature_command_template": "{{ value }}", + "temperature_state_topic": "temperature-state-topic", + "temperature_state_template": "{{ value_json.temperature }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, + # power settings + "climate_power_settings": { + "power_command_topic": "power-command-topic", + "power_command_template": "{{ value }}", + "payload_on": "ON", + "payload_off": "OFF", + }, + # current action settings + "climate_action_settings": { + "action_topic": "action-topic", + "action_template": "{{ value_json.current_action }}", + }, + # target humidity + "target_humidity_settings": { + "target_humidity_command_topic": "target-humidity-command-topic", + "target_humidity_command_template": "{{ value }}", + "target_humidity_state_topic": "target-humidity-state-topic", + "target_humidity_state_template": "{{ value_json.target_humidity }}", + "min_humidity": 20, + "max_humidity": 80, + }, + # current temperature + "current_temperature_settings": { + "current_temperature_topic": "current-temperature-topic", + "current_temperature_template": "{{ value_json.temperature }}", + }, + # current humidity + "current_humidity_settings": { + "current_humidity_topic": "current-humidity-topic", + "current_humidity_template": "{{ value_json.humidity }}", + }, + # preset mode + "climate_preset_mode_settings": { + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_command_template": "{{ value }}", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "preset_modes": ["auto", "eco"], + }, + # fan mode + "climate_fan_mode_settings": { + "fan_mode_command_topic": "fan-mode-command-topic", + "fan_mode_command_template": "{{ value }}", + "fan_mode_state_topic": "fan-mode-state-topic", + "fan_mode_state_template": "{{ value_json.fan_mode }}", + "fan_modes": ["off", "low", "medium", "high"], + }, + # swing mode + "climate_swing_mode_settings": { + "swing_mode_command_topic": "swing-mode-command-topic", + "swing_mode_command_template": "{{ value }}", + "swing_mode_state_topic": "swing-mode-state-topic", + "swing_mode_state_template": "{{ value_json.swing_mode }}", + "swing_modes": ["off", "on"], + }, + # swing horizontal mode + "climate_swing_horizontal_mode_settings": { + "swing_horizontal_mode_command_topic": "swing-horizontal-mode-command-topic", + "swing_horizontal_mode_command_template": "{{ value }}", + "swing_horizontal_mode_state_topic": "swing-horizontal-mode-state-topic", + "swing_horizontal_mode_state_template": "{{ value_json.swing_horizontal_mode }}", + "swing_horizontal_modes": ["off", "on"], + }, + }, + ( + ( + { + "modes": ["off", "heat", "cool", "auto"], + "target_temperature_settings": { + "temperature_command_topic": "test-topic#invalid" + }, + }, + {"target_temperature_settings": "invalid_publish_topic"}, + ), + ( + { + "modes": [], + "target_temperature_settings": { + "temperature_command_topic": "test-topic" + }, + }, + {"modes": "empty_list_not_allowed"}, + ), + ( + { + "modes": ["off", "heat", "cool", "auto"], + "target_temperature_settings": { + "temperature_command_topic": "test-topic", + "min_temp": 19.0, + "max_temp": 18.0, + }, + "target_humidity_settings": { + "target_humidity_command_topic": "test-topic", + "min_humidity": 50, + "max_humidity": 40, + }, + "climate_preset_mode_settings": { + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["none"], + }, + }, + { + "target_temperature_settings": "max_below_min_temperature", + "target_humidity_settings": "max_below_min_humidity", + "climate_preset_mode_settings": "preset_mode_none_not_allowed", + }, + ), + ), + "Milk notifier Cooler", + ), + ( + MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Cooler"}, + { + "temperature_unit": "C", + "climate_feature_action": False, + "climate_feature_current_humidity": False, + "climate_feature_current_temperature": False, + "climate_feature_power": False, + "climate_feature_preset_modes": False, + "climate_feature_fan_modes": False, + "climate_feature_swing_horizontal_modes": False, + "climate_feature_swing_modes": False, + "climate_feature_target_temperature": "high_low", + "climate_feature_target_humidity": False, + }, + (), + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + # high/low target temperature + "target_temperature_settings": { + "temperature_low_command_topic": "temperature-low-command-topic", + "temperature_low_command_template": "{{ value }}", + "temperature_low_state_topic": "temperature-low-state-topic", + "temperature_low_state_template": "{{ value_json.temperature_low }}", + "temperature_high_command_topic": "temperature-high-command-topic", + "temperature_high_command_template": "{{ value }}", + "temperature_high_state_topic": "temperature-high-state-topic", + "temperature_high_state_template": "{{ value_json.temperature_high }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, + }, + (), + "Milk notifier Cooler", + ), + ( + MOCK_CLIMATE_NO_TARGET_TEMP_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Cooler"}, + { + "temperature_unit": "C", + "climate_feature_action": False, + "climate_feature_current_humidity": False, + "climate_feature_current_temperature": False, + "climate_feature_power": False, + "climate_feature_preset_modes": False, + "climate_feature_fan_modes": False, + "climate_feature_swing_horizontal_modes": False, + "climate_feature_swing_modes": False, + "climate_feature_target_temperature": "none", + "climate_feature_target_humidity": False, + }, + (), + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + }, + (), + "Milk notifier Cooler", + ), ( MOCK_COVER_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, @@ -3130,6 +3351,9 @@ async def test_migrate_of_incompatible_config_entry( ids=[ "binary_sensor", "button", + "climate_single", + "climate_high_low", + "climate_no_target_temp", "cover", "fan", "notify_with_entity_name", @@ -3631,8 +3855,144 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites( }, {"optimistic", "state_value_template", "entity_picture"}, ), + ( + ( + ConfigSubentryData( + data=MOCK_CLIMATE_SUBENTRY_DATA_SINGLE, + subentry_type="device", + title="Mock subentry", + ), + ), + (), + { + "climate_feature_action": False, + "climate_feature_current_humidity": False, + "climate_feature_current_temperature": False, + "climate_feature_power": False, + "climate_feature_preset_modes": False, + "climate_feature_fan_modes": False, + "climate_feature_swing_horizontal_modes": False, + "climate_feature_swing_modes": False, + "climate_feature_target_temperature": "high_low", + "climate_feature_target_humidity": False, + }, + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool"], + # high/low target temperature + "target_temperature_settings": { + "temperature_low_command_topic": "temperature-low-command-topic", + "temperature_low_command_template": "{{ value }}", + "temperature_low_state_topic": "temperature-low-state-topic", + "temperature_low_state_template": "{{ value_json.temperature_low }}", + "temperature_high_command_topic": "temperature-high-command-topic", + "temperature_high_command_template": "{{ value }}", + "temperature_high_state_topic": "temperature-high-state-topic", + "temperature_high_state_template": "{{ value_json.temperature_high }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, + }, + {}, + { + "current_humidity_topic", + "action_topic", + "swing_modes", + "max_humidity", + "fan_modes", + "action_template", + "current_temperature_template", + "temperature_state_template", + "entity_picture", + "target_humidity_state_template", + "fan_mode_state_topic", + "swing_horizontal_mode_command_template", + "power_command_template", + "swing_horizontal_modes", + "current_temperature_topic", + "temperature_command_topic", + "swing_mode_command_topic", + "fan_mode_command_template", + "swing_horizontal_mode_state_template", + "preset_mode_command_template", + "swing_mode_command_template", + "temperature_state_topic", + "preset_mode_value_template", + "fan_mode_state_template", + "swing_horizontal_mode_command_topic", + "min_humidity", + "temperature_command_template", + "preset_modes", + "swing_horizontal_mode_state_topic", + "target_humidity_state_topic", + "target_humidity_command_topic", + "preset_mode_command_topic", + "payload_on", + "payload_off", + "power_command_topic", + "current_humidity_template", + "preset_mode_state_topic", + "fan_mode_command_topic", + "swing_mode_state_template", + "target_humidity_command_template", + "swing_mode_state_topic", + }, + ), + ( + ( + ConfigSubentryData( + data=MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE, + subentry_type="device", + title="Mock subentry", + ), + ), + (), + { + "climate_feature_action": False, + "climate_feature_current_humidity": False, + "climate_feature_current_temperature": False, + "climate_feature_power": False, + "climate_feature_preset_modes": False, + "climate_feature_fan_modes": False, + "climate_feature_swing_horizontal_modes": False, + "climate_feature_swing_modes": False, + "climate_feature_target_temperature": "high_low", + "climate_feature_target_humidity": False, + }, + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool"], + # high/low target temperature + "target_temperature_settings": { + "temperature_low_command_topic": "temperature-low-command-topic", + "temperature_low_command_template": "{{ value }}", + "temperature_low_state_topic": "temperature-low-state-topic", + "temperature_low_state_template": "{{ value_json.temperature_low }}", + "temperature_high_command_topic": "temperature-high-command-topic", + "temperature_high_command_template": "{{ value }}", + "temperature_high_state_topic": "temperature-high-state-topic", + "temperature_high_state_template": "{{ value_json.temperature_high }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, + }, + {}, + {"entity_picture"}, + ), ], - ids=["notify", "sensor", "light_basic"], + ids=["notify", "sensor", "light_basic", "climate_single", "climate_high_low"], ) async def test_subentry_reconfigure_edit_entity_single_entity( hass: HomeAssistant, From 70cfdfa231f169d61a4687d5fe912e22d40f3b99 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:23:54 +0200 Subject: [PATCH 128/265] Remove unnecessary CONFIG_SCHEMA from Uptime Kuma integration (#149601) --- homeassistant/components/uptime_kuma/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/uptime_kuma/__init__.py b/homeassistant/components/uptime_kuma/__init__.py index 4efe6a68193..cdeae16cc5a 100644 --- a/homeassistant/components/uptime_kuma/__init__.py +++ b/homeassistant/components/uptime_kuma/__init__.py @@ -6,7 +6,7 @@ from pythonkuma.update import UpdateChecker from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.hass_dict import HassKey @@ -19,7 +19,6 @@ from .coordinator import ( _PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.UPDATE] -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) UPTIME_KUMA_KEY: HassKey[UptimeKumaSoftwareUpdateCoordinator] = HassKey(DOMAIN) From a21af78aa1391e815dd360dbfd46acfca84a9b01 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:27:43 -0400 Subject: [PATCH 129/265] Add config flow to template light platform (#149448) --- .../components/template/config_flow.py | 33 ++++++++++ homeassistant/components/template/light.py | 50 ++++++++++++++- .../components/template/strings.json | 55 +++++++++++++++++ .../template/snapshots/test_light.ambr | 19 ++++++ tests/components/template/test_config_flow.py | 32 ++++++++++ tests/components/template/test_light.py | 61 ++++++++++++++++++- 6 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 tests/components/template/snapshots/test_light.ambr diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 7963f525b7a..c9028d058bf 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -72,6 +72,15 @@ from .cover import ( STOP_ACTION, async_create_preview_cover, ) +from .light import ( + CONF_HS, + CONF_HS_ACTION, + CONF_LEVEL, + CONF_LEVEL_ACTION, + CONF_TEMPERATURE, + CONF_TEMPERATURE_ACTION, + async_create_preview_light, +) from .number import ( CONF_MAX, CONF_MIN, @@ -179,6 +188,18 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: vol.Optional(CONF_VERIFY_SSL, default=True): selector.BooleanSelector(), } + if domain == Platform.LIGHT: + schema |= _SCHEMA_STATE | { + vol.Required(CONF_TURN_ON): selector.ActionSelector(), + vol.Required(CONF_TURN_OFF): selector.ActionSelector(), + vol.Optional(CONF_LEVEL): selector.TemplateSelector(), + vol.Optional(CONF_LEVEL_ACTION): selector.ActionSelector(), + vol.Optional(CONF_HS): selector.TemplateSelector(), + vol.Optional(CONF_HS_ACTION): selector.ActionSelector(), + vol.Optional(CONF_TEMPERATURE): selector.TemplateSelector(), + vol.Optional(CONF_TEMPERATURE_ACTION): selector.ActionSelector(), + } + if domain == Platform.NUMBER: schema |= { vol.Required(CONF_STATE): selector.TemplateSelector(), @@ -359,6 +380,7 @@ TEMPLATE_TYPES = [ Platform.BUTTON, Platform.COVER, Platform.IMAGE, + Platform.LIGHT, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, @@ -391,6 +413,11 @@ CONFIG_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.LIGHT: SchemaFlowFormStep( + config_schema(Platform.LIGHT), + preview="template", + validate_user_input=validate_user_input(Platform.LIGHT), + ), Platform.NUMBER: SchemaFlowFormStep( config_schema(Platform.NUMBER), preview="template", @@ -440,6 +467,11 @@ OPTIONS_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.LIGHT: SchemaFlowFormStep( + options_schema(Platform.LIGHT), + preview="template", + validate_user_input=validate_user_input(Platform.LIGHT), + ), Platform.NUMBER: SchemaFlowFormStep( options_schema(Platform.NUMBER), preview="template", @@ -469,6 +501,7 @@ CREATE_PREVIEW_ENTITY: dict[ Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel, Platform.BINARY_SENSOR: async_create_preview_binary_sensor, Platform.COVER: async_create_preview_cover, + Platform.LIGHT: async_create_preview_light, Platform.NUMBER: async_create_preview_number, Platform.SELECT: async_create_preview_select, Platform.SENSOR: async_create_preview_sensor, diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 19eecaa7006..538d3f3aaaf 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -27,6 +27,7 @@ from homeassistant.components.light import ( LightEntityFeature, filter_supported_color_modes, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EFFECT, CONF_ENTITY_ID, @@ -43,15 +44,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import color as color_util from . import TriggerUpdateCoordinator from .const import DOMAIN from .entity import AbstractTemplateEntity -from .helpers import async_setup_template_platform +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) from .template_entity import ( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, @@ -135,6 +144,8 @@ LIGHT_COMMON_SCHEMA = vol.Schema( vol.Optional(CONF_MIN_MIREDS): cv.template, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGB): cv.template, vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA, @@ -195,6 +206,10 @@ PLATFORM_SCHEMA = vol.All( ), ) +LIGHT_CONFIG_ENTRY_SCHEMA = LIGHT_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + async def async_setup_platform( hass: HomeAssistant, @@ -216,6 +231,37 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + StateLightEntity, + LIGHT_CONFIG_ENTRY_SCHEMA, + True, + ) + + +@callback +def async_create_preview_light( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateLightEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateLightEntity, + LIGHT_CONFIG_ENTRY_SCHEMA, + True, + ) + + class AbstractTemplateLight(AbstractTemplateEntity, LightEntity): """Representation of a template lights features.""" diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 36bca174ef6..070dd75865f 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -131,6 +131,33 @@ }, "title": "Template image" }, + "light": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "level": "Brightness level", + "set_level": "Actions on set level", + "hs": "HS color", + "set_hs": "Actions on set HS color", + "temperature": "Color temperature", + "set_temperature": "Actions on set color temperature" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "Template light" + }, "number": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -206,6 +233,7 @@ "button": "Template a button", "cover": "Template a cover", "image": "Template an image", + "light": "Template a light", "number": "Template a number", "select": "Template a select", "sensor": "Template a sensor", @@ -351,6 +379,33 @@ }, "title": "[%key:component::template::config::step::image::title%]" }, + "light": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "level": "[%key:component::template::config::step::light::data::level%]", + "set_level": "[%key:component::template::config::step::light::data::set_level%]", + "hs": "[%key:component::template::config::step::light::data::hs%]", + "set_hs": "[%key:component::template::config::step::light::data::set_hs%]", + "temperature": "[%key:component::template::config::step::light::data::temperature%]", + "set_temperature": "[%key:component::template::config::step::light::data::set_temperature%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "[%key:component::template::config::step::light::title%]" + }, "number": { "data": { "device_id": "[%key:common::config_flow::data::device%]", diff --git a/tests/components/template/snapshots/test_light.ambr b/tests/components/template/snapshots/test_light.ambr new file mode 100644 index 00000000000..0740d56a72e --- /dev/null +++ b/tests/components/template/snapshots/test_light.ambr @@ -0,0 +1,19 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'My template', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 8d7f2e6d89c..ad992eec79d 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -159,6 +159,16 @@ BINARY_SENSOR_OPTIONS = { {"verify_ssl": True}, {}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + "on", + {"one": "on", "two": "off"}, + {}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + {}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -330,6 +340,12 @@ async def test_config_flow( {"verify_ssl": True}, {"verify_ssl": True}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -535,6 +551,16 @@ async def test_config_flow_device( }, "url", ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"state": "{{ states('light.two') }}"}, + ["on", "off"], + {"one": "on", "two": "off"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + "state", + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -1374,6 +1400,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), ( "number", {"state": "{{ states('number.one') }}"}, diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index b42eba0665d..0549f9981e7 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import light, template from homeassistant.components.light import ( @@ -30,9 +31,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import ConfigurationStyle +from .conftest import ConfigurationStyle, async_get_flow_preview_state -from tests.common import assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component +from tests.typing import WebSocketGenerator # Represent for light's availability _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" @@ -2791,3 +2793,58 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a light from a config entry.""" + + hass.states.async_set( + "sensor.test_sensor", + "on", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('sensor.test_sensor') }}", + "turn_on": [], + "turn_off": [], + "template_type": light.DOMAIN, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("light.my_template") + assert state is not None + assert state == snapshot + + +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + light.DOMAIN, + { + "name": "My template", + "state": "{{ 'on' }}", + "turn_on": [], + "turn_off": [], + }, + ) + + assert state["state"] == STATE_ON From 91be25a292f96b5430b6d8c47a60d7c11076f167 Mon Sep 17 00:00:00 2001 From: lucasfijen Date: Wed, 30 Jul 2025 15:43:10 +0200 Subject: [PATCH 130/265] Add get recipes search service to Mealie integration (#149348) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/mealie/const.py | 2 + homeassistant/components/mealie/icons.json | 3 + homeassistant/components/mealie/services.py | 39 + homeassistant/components/mealie/services.yaml | 21 + homeassistant/components/mealie/strings.json | 21 + tests/components/mealie/conftest.py | 3 + .../mealie/fixtures/get_recipes.json | 1692 +++++++++++++++++ .../mealie/snapshots/test_services.ambr | 1238 ++++++++++++ tests/components/mealie/test_services.py | 60 + 9 files changed, 3079 insertions(+) create mode 100644 tests/components/mealie/fixtures/get_recipes.json diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py index c040d665794..481cc4ccb7d 100644 --- a/homeassistant/components/mealie/const.py +++ b/homeassistant/components/mealie/const.py @@ -17,5 +17,7 @@ ATTR_INCLUDE_TAGS = "include_tags" ATTR_ENTRY_TYPE = "entry_type" ATTR_NOTE_TITLE = "note_title" ATTR_NOTE_TEXT = "note_text" +ATTR_SEARCH_TERMS = "search_terms" +ATTR_RESULT_LIMIT = "result_limit" MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0") diff --git a/homeassistant/components/mealie/icons.json b/homeassistant/components/mealie/icons.json index d7e29cc8bbe..773d70afa5f 100644 --- a/homeassistant/components/mealie/icons.json +++ b/homeassistant/components/mealie/icons.json @@ -30,6 +30,9 @@ "get_recipe": { "service": "mdi:map" }, + "get_recipes": { + "service": "mdi:book-open-page-variant" + }, "import_recipe": { "service": "mdi:map-search" }, diff --git a/homeassistant/components/mealie/services.py b/homeassistant/components/mealie/services.py index 0d9a29392a4..f219cea1835 100644 --- a/homeassistant/components/mealie/services.py +++ b/homeassistant/components/mealie/services.py @@ -32,6 +32,8 @@ from .const import ( ATTR_NOTE_TEXT, ATTR_NOTE_TITLE, ATTR_RECIPE_ID, + ATTR_RESULT_LIMIT, + ATTR_SEARCH_TERMS, ATTR_START_DATE, ATTR_URL, DOMAIN, @@ -55,6 +57,15 @@ SERVICE_GET_RECIPE_SCHEMA = vol.Schema( } ) +SERVICE_GET_RECIPES = "get_recipes" +SERVICE_GET_RECIPES_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Optional(ATTR_SEARCH_TERMS): str, + vol.Optional(ATTR_RESULT_LIMIT): int, + } +) + SERVICE_IMPORT_RECIPE = "import_recipe" SERVICE_IMPORT_RECIPE_SCHEMA = vol.Schema( { @@ -159,6 +170,27 @@ async def _async_get_recipe(call: ServiceCall) -> ServiceResponse: return {"recipe": asdict(recipe)} +async def _async_get_recipes(call: ServiceCall) -> ServiceResponse: + """Get recipes.""" + entry = _async_get_entry(call) + search_terms = call.data.get(ATTR_SEARCH_TERMS) + result_limit = call.data.get(ATTR_RESULT_LIMIT, 10) + client = entry.runtime_data.client + try: + recipes = await client.get_recipes(search=search_terms, per_page=result_limit) + except MealieConnectionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="connection_error", + ) from err + except MealieNotFoundError as err: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_recipes_found", + ) from err + return {"recipes": asdict(recipes)} + + async def _async_import_recipe(call: ServiceCall) -> ServiceResponse: """Import a recipe.""" entry = _async_get_entry(call) @@ -242,6 +274,13 @@ def async_setup_services(hass: HomeAssistant) -> None: schema=SERVICE_GET_RECIPE_SCHEMA, supports_response=SupportsResponse.ONLY, ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_RECIPES, + _async_get_recipes, + schema=SERVICE_GET_RECIPES_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) hass.services.async_register( DOMAIN, SERVICE_IMPORT_RECIPE, diff --git a/homeassistant/components/mealie/services.yaml b/homeassistant/components/mealie/services.yaml index 47a79ba5756..6a78564a578 100644 --- a/homeassistant/components/mealie/services.yaml +++ b/homeassistant/components/mealie/services.yaml @@ -24,6 +24,27 @@ get_recipe: selector: text: +get_recipes: + fields: + config_entry_id: + required: true + selector: + config_entry: + integration: mealie + search_terms: + required: false + selector: + text: + result_limit: + required: false + default: 10 + selector: + number: + min: 1 + max: 100 + mode: box + unit_of_measurement: recipes + import_recipe: fields: config_entry_id: diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 186fc4c4ac0..5533631f755 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -109,6 +109,9 @@ "recipe_not_found": { "message": "Recipe with ID or slug `{recipe_id}` not found." }, + "no_recipes_found": { + "message": "No recipes found matching your search." + }, "could_not_import_recipe": { "message": "Mealie could not import the recipe from the URL." }, @@ -176,6 +179,24 @@ } } }, + "get_recipes": { + "name": "Get recipes", + "description": "Searches for recipes with any matching properties in Mealie", + "fields": { + "config_entry_id": { + "name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]", + "description": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::description%]" + }, + "search_terms": { + "name": "Search terms", + "description": "Terms to search for in recipe properties." + }, + "result_limit": { + "name": "Result limit", + "description": "Maximum number of recipes to return (default: 10)." + } + } + }, "import_recipe": { "name": "Import recipe", "description": "Imports a recipe from an URL", diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index 8e724e4d8ea..422b1c3de44 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -8,6 +8,7 @@ from aiomealie import ( Mealplan, MealplanResponse, Recipe, + RecipesResponse, ShoppingItemsResponse, ShoppingListsResponse, Statistics, @@ -63,6 +64,8 @@ def mock_mealie_client() -> Generator[AsyncMock]: ) recipe = Recipe.from_json(load_fixture("get_recipe.json", DOMAIN)) client.get_recipe.return_value = recipe + recipes = RecipesResponse.from_json(load_fixture("get_recipes.json", DOMAIN)) + client.get_recipes.return_value = recipes client.import_recipe.return_value = recipe client.get_shopping_lists.return_value = ShoppingListsResponse.from_json( load_fixture("get_shopping_lists.json", DOMAIN) diff --git a/tests/components/mealie/fixtures/get_recipes.json b/tests/components/mealie/fixtures/get_recipes.json new file mode 100644 index 00000000000..8ee91a1aa0e --- /dev/null +++ b/tests/components/mealie/fixtures/get_recipes.json @@ -0,0 +1,1692 @@ +{ + "page": 1, + "per_page": 50, + "total": 662, + "total_pages": 14, + "items": [ + { + "id": "e82f5449-c33b-437c-b712-337587199264", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "tu6y", + "slug": "tu6y", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T11:10:14.866359", + "createdAt": "2024-01-21T11:10:14.880721", + "updateAt": "2024-01-21T11:10:14.880723", + "lastMade": null + }, + { + "id": "f79f7e9d-4b58-4930-a586-2b127f16ee34", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)", + "slug": "eukole-makaronada-me-kephtedakia-ston-phourno-1", + "image": "En9o", + "recipeYield": "6 servings", + "totalTime": null, + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "50 Minutes", + "description": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T09:08:58.056854", + "createdAt": "2024-01-21T09:08:58.059401", + "updateAt": "2024-01-21T09:08:58.059403", + "lastMade": null + }, + { + "id": "90097c8b-9d80-468a-b497-73957ac0cd8b", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Patates douces au four (1)", + "slug": "patates-douces-au-four-1", + "image": "aAhk", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T10:27:39.409746", + "createdAt": "2024-01-21T09:08:53.846294", + "updateAt": "2024-01-21T09:08:53.846295", + "lastMade": null + }, + { + "id": "98845807-9365-41fd-acd1-35630b468c27", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Sweet potatoes", + "slug": "sweet-potatoes", + "image": "kdhm", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T10:28:05.977615", + "createdAt": "2024-01-21T09:08:53.846294", + "updateAt": "2024-01-21T09:08:53.846295", + "lastMade": null + }, + { + "id": "40c227e0-3c7e-41f7-866d-5de04eaecdd7", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο", + "slug": "eukole-makaronada-me-kephtedakia-ston-phourno", + "image": "tNbG", + "recipeYield": "6 servings", + "totalTime": null, + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "50 Minutes", + "description": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T09:06:44.015829", + "createdAt": "2024-01-21T09:06:44.019650", + "updateAt": "2024-01-21T09:06:44.019653", + "lastMade": null + }, + { + "id": "9c7b8aee-c93c-4b1b-ab48-2625d444743a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Boeuf bourguignon : la vraie recette (2)", + "slug": "boeuf-bourguignon-la-vraie-recette-2", + "image": "nj5M", + "recipeYield": "4 servings", + "totalTime": "5 Hours", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": "4 Hours", + "description": "bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre", + "recipeCategory": [], + "tags": [ + { + "id": "01c2f4ac-54ce-49bc-9bd7-8a49f353a3a4", + "name": "Poivre", + "slug": "poivre" + }, + { + "id": "90a26cea-a8a1-41a1-9e8c-e94e3c40f7a7", + "name": "Sel", + "slug": "sel" + }, + { + "id": "d7b01a4b-5206-4bd2-b9c4-d13b95ca0edb", + "name": "Beurre", + "slug": "beurre" + }, + { + "id": "304faaf8-13ec-4537-91f3-9f39a3585545", + "name": "Facile", + "slug": "facile" + }, + { + "id": "6508fb05-fb60-4bed-90c4-584bd6d74cb5", + "name": "Daube", + "slug": "daube" + }, + { + "id": "18ff59b6-b599-456a-896b-4b76448b08ca", + "name": "Bourguignon", + "slug": "bourguignon" + }, + { + "id": "685a0d90-8de4-494e-8eb8-68e7f5d5ffbe", + "name": "Vin Rouge", + "slug": "vin-rouge" + }, + { + "id": "5dedc8b5-30f5-4d6e-875f-34deefd01883", + "name": "Oignon", + "slug": "oignon" + }, + { + "id": "065b79e0-6276-4ebb-9428-7018b40c55bb", + "name": "Bouquet Garni", + "slug": "bouquet-garni" + }, + { + "id": "d858b1d9-2ca1-46d4-acc2-3d03f991f03f", + "name": "Moyen", + "slug": "moyen" + }, + { + "id": "bded0bd8-8d41-4ec5-ad73-e0107fb60908", + "name": "Boeuf Bourguignon : La Vraie Recette", + "slug": "boeuf-bourguignon-la-vraie-recette" + }, + { + "id": "7f99b04f-914a-408b-a057-511ca1125734", + "name": "Carotte", + "slug": "carotte" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:45:28.780361", + "createdAt": "2024-01-21T08:45:28.782322", + "updateAt": "2024-01-21T08:45:28.782324", + "lastMade": null + }, + { + "id": "fc42c7d1-7b0f-4e04-b88a-dbd80b81540b", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Boeuf bourguignon : la vraie recette (1)", + "slug": "boeuf-bourguignon-la-vraie-recette-1", + "image": "rbU7", + "recipeYield": "4 servings", + "totalTime": "5 Hours", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": "4 Hours", + "description": "bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre", + "recipeCategory": [], + "tags": [ + { + "id": "01c2f4ac-54ce-49bc-9bd7-8a49f353a3a4", + "name": "Poivre", + "slug": "poivre" + }, + { + "id": "90a26cea-a8a1-41a1-9e8c-e94e3c40f7a7", + "name": "Sel", + "slug": "sel" + }, + { + "id": "d7b01a4b-5206-4bd2-b9c4-d13b95ca0edb", + "name": "Beurre", + "slug": "beurre" + }, + { + "id": "304faaf8-13ec-4537-91f3-9f39a3585545", + "name": "Facile", + "slug": "facile" + }, + { + "id": "6508fb05-fb60-4bed-90c4-584bd6d74cb5", + "name": "Daube", + "slug": "daube" + }, + { + "id": "18ff59b6-b599-456a-896b-4b76448b08ca", + "name": "Bourguignon", + "slug": "bourguignon" + }, + { + "id": "685a0d90-8de4-494e-8eb8-68e7f5d5ffbe", + "name": "Vin Rouge", + "slug": "vin-rouge" + }, + { + "id": "5dedc8b5-30f5-4d6e-875f-34deefd01883", + "name": "Oignon", + "slug": "oignon" + }, + { + "id": "065b79e0-6276-4ebb-9428-7018b40c55bb", + "name": "Bouquet Garni", + "slug": "bouquet-garni" + }, + { + "id": "d858b1d9-2ca1-46d4-acc2-3d03f991f03f", + "name": "Moyen", + "slug": "moyen" + }, + { + "id": "bded0bd8-8d41-4ec5-ad73-e0107fb60908", + "name": "Boeuf Bourguignon : La Vraie Recette", + "slug": "boeuf-bourguignon-la-vraie-recette" + }, + { + "id": "7f99b04f-914a-408b-a057-511ca1125734", + "name": "Carotte", + "slug": "carotte" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:43:36.105722", + "createdAt": "2024-01-21T08:43:36.108116", + "updateAt": "2024-01-21T08:43:36.108118", + "lastMade": null + }, + { + "id": "89e63d72-7a51-4cef-b162-2e45035d0a91", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Veganes Marmor-Bananenbrot mit Erdnussbutter", + "slug": "veganes-marmor-bananenbrot-mit-erdnussbutter", + "image": "JSp3", + "recipeYield": "14 servings", + "totalTime": null, + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "55 Minutes", + "description": "Dieses einfache vegane Erdnussbutter-Schoko-Marmor-Bananenbrot Rezept enthält kein Öl und keinen raffiniernten Zucker, ist aber so fluffig, weich, saftig und lecker wie ein Kuchen! Zubereitet mit vielen gesunden Bananen, gelingt es auch glutenfrei und eignet sich perfekt zum Frühstück, als Dessert oder Snack für Zwischendurch!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://biancazapatka.com/de/erdnussbutter-schoko-bananenbrot/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:28:11.008440", + "createdAt": "2024-01-21T08:28:11.011427", + "updateAt": "2024-01-21T08:28:11.011428", + "lastMade": null + }, + { + "id": "eab64457-97ba-4d6c-871c-cb1c724ccb51", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Pasta mit Tomaten, Knoblauch und Basilikum - einfach (und) genial! - Kuechenchaotin", + "slug": "pasta-mit-tomaten-knoblauch-und-basilikum-einfach-und-genial-kuechenchaotin", + "image": "9QMh", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Es ist kein Geheimnis: Ich mag es gerne schnell und einfach. Und ich liebe Pasta! Deshalb habe ich mich vor ein paar Wochen auf die Suche nach der perfekten, schnellen Tomatensoße gemacht. Es muss da draußen doch irgendein Rezept geben, das (fast) genauso schnell zuzubereiten ist, wie Miracoli und dabei aber das schöne Gefühl hinterlässt, ...", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://kuechenchaotin.de/pasta-mit-tomaten-knoblauch-basilikum/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:24:50.952774", + "createdAt": "2024-01-21T08:24:50.955843", + "updateAt": "2024-01-21T08:24:50.955845", + "lastMade": null + }, + { + "id": "12439e3d-3c1c-4dcc-9c6e-4afcea2a0542", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "test123", + "slug": "test123", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:00:02.755328", + "createdAt": "2024-01-21T08:00:02.757103", + "updateAt": "2024-01-21T08:00:02.757105", + "lastMade": null + }, + { + "id": "6567f6ec-e410-49cb-a1a5-d08517184e78", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Bureeto", + "slug": "bureeto", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:37:39.940578", + "createdAt": "2024-01-21T07:37:39.942535", + "updateAt": "2024-01-21T07:37:39.942537", + "lastMade": null + }, + { + "id": "f7737d17-161c-4008-88d4-dd2616778cd0", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Subway Double Cookies", + "slug": "subway-double-cookies", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:34:53.944858", + "createdAt": "2024-01-21T07:34:53.946852", + "updateAt": "2024-01-21T07:34:53.946854", + "lastMade": null + }, + { + "id": "1904b717-4a8b-4de9-8909-56958875b5f4", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "qwerty12345", + "slug": "qwerty12345", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:37:55.795675", + "createdAt": "2024-01-21T07:28:05.395272", + "updateAt": "2024-01-21T07:28:05.395274", + "lastMade": null + }, + { + "id": "8bdd3656-5e7e-45d3-a3c4-557390846a22", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Cheeseburger Sliders (Easy, 30-min Recipe)", + "slug": "cheeseburger-sliders-easy-30-min-recipe", + "image": "beGq", + "recipeYield": "24 servings", + "totalTime": "30 Minutes", + "prepTime": "8 Minutes", + "cookTime": null, + "performTime": "22 Minutes", + "description": "Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.", + "recipeCategory": [], + "tags": [ + { + "id": "7a4ca427-642f-4428-8dc7-557ea9c8d1b4", + "name": "Cheeseburger Sliders", + "slug": "cheeseburger-sliders" + }, + { + "id": "941558d2-50d5-4c9d-8890-a0258f18d493", + "name": "Sliders", + "slug": "sliders" + } + ], + "tools": [], + "rating": 5, + "orgURL": "https://natashaskitchen.com/cheeseburger-sliders/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:43:24.261010", + "createdAt": "2024-01-21T06:49:35.466777", + "updateAt": "2024-01-21T06:49:35.466778", + "lastMade": "2024-01-22T04:59:59" + }, + { + "id": "8a30d31d-aa14-411e-af0c-6b61a94f5291", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "meatloaf", + "slug": "meatloaf", + "image": null, + "recipeYield": "4", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T06:37:09.426467", + "createdAt": "2024-01-21T06:36:57.645658", + "updateAt": "2024-01-21T06:37:09.428351", + "lastMade": null + }, + { + "id": "f2f7880b-1136-436f-91b7-129788d8c117", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Richtig rheinischer Sauerbraten", + "slug": "richtig-rheinischer-sauerbraten", + "image": "kCBh", + "recipeYield": "4 servings", + "totalTime": "3 Hours 20 Minutes", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": "2 Hours 20 Minutes", + "description": "Richtig rheinischer Sauerbraten - Rheinischer geht's nicht! Über 536 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": 3, + "orgURL": "https://www.chefkoch.de/rezepte/937641199437984/Richtig-rheinischer-Sauerbraten.html", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T05:37:55.419788", + "createdAt": "2024-01-21T05:24:03.402973", + "updateAt": "2024-01-21T05:37:55.422471", + "lastMade": null + }, + { + "id": "cf634591-0f82-4254-8e00-2f7e8b0c9022", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Orientalischer Gemüse-Hähnchen Eintopf", + "slug": "orientalischer-gemuse-hahnchen-eintopf", + "image": "kpBx", + "recipeYield": "6 servings", + "totalTime": "35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Orientalischer Gemüse-Hähnchen Eintopf. Über 164 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!", + "recipeCategory": [], + "tags": [ + { + "id": "518f3081-a919-4c80-9cad-75ffbd0e73d3", + "name": "Gemüse", + "slug": "gemuse" + }, + { + "id": "a3fff625-1902-4112-b169-54aec4f52ea7", + "name": "Hauptspeise", + "slug": "hauptspeise" + }, + { + "id": "4c79c0b7-c2d0-415a-b5cf-138cfce92c7e", + "name": "Einfach", + "slug": "einfach" + }, + { + "id": "1f87d43d-7d9d-4806-993a-fdb89117d64e", + "name": "Fleisch", + "slug": "fleisch" + }, + { + "id": "7caa64df-c65d-4fb0-9075-b788e6a05e1d", + "name": "Geflügel", + "slug": "geflugel" + }, + { + "id": "38d18d57-d817-491e-94f8-da923d2c540e", + "name": "Eintopf", + "slug": "eintopf" + }, + { + "id": "398fbd98-4175-4652-92a4-51e55482dc9b", + "name": "Schmoren", + "slug": "schmoren" + }, + { + "id": "ec303c13-a4f7-4de3-8a4f-d13b72ddd500", + "name": "Hülsenfrüchte", + "slug": "hulsenfruchte" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/2307761368177614/Orientalischer-Gemuese-Haehnchen-Eintopf.html", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:58:54.661618", + "createdAt": "2024-01-21T04:58:54.665601", + "updateAt": "2024-01-21T04:58:54.665603", + "lastMade": null + }, + { + "id": "05208856-d273-4cc9-bcfa-e0215d57108d", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "test 20240121", + "slug": "test-20240121", + "image": null, + "recipeYield": "4", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:56:20.569413", + "createdAt": "2024-01-21T04:55:49.820247", + "updateAt": "2024-01-21T04:56:20.571564", + "lastMade": null + }, + { + "id": "145eeb05-781a-4eb0-a656-afa8bc8c0164", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Loempia bowl", + "slug": "loempia-bowl", + "image": "McEx", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Zet in 20 minuten deze lekkere loempia bowl in elkaar. Makkelijk, snel en weer eens wat anders. Lekker met prei, sojasaus en kipgehakt.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.lekkerensimpel.com/loempia-bowl/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:39:48.558572", + "createdAt": "2024-01-21T04:39:48.560422", + "updateAt": "2024-01-21T04:39:48.560424", + "lastMade": null + }, + { + "id": "5c6532aa-ad84-424c-bc05-c32d50430fe4", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "5 Ingredient Chocolate Mousse", + "slug": "5-ingredient-chocolate-mousse", + "image": "bzqo", + "recipeYield": "6 servings", + "totalTime": null, + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": null, + "description": "Chocolate Mousse with Aquafaba, to make the fluffiest of mousses. Whip up this dessert in literally five minutes and chill in the fridge until you're ready to serve!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://thehappypear.ie/aquafaba-chocolate-mousse/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T06:06:26.305680", + "createdAt": "2024-01-21T04:14:34.624708", + "updateAt": "2024-01-21T06:06:26.308017", + "lastMade": null + }, + { + "id": "f2e684f2-49e0-45ee-90de-951344472f1c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Der perfekte Pfannkuchen - gelingt einfach immer", + "slug": "der-perfekte-pfannkuchen-gelingt-einfach-immer", + "image": "KGK6", + "recipeYield": "4 servings", + "totalTime": "15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "10 Minutes", + "description": "Der perfekte Pfannkuchen - gelingt einfach immer - von Kindern geliebt und auch für Kochneulinge super geeignet. Über 2529 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + "recipeCategory": [], + "tags": [ + { + "id": "4ec445c6-fc2f-4a1e-b666-93435a46ec42", + "name": "Schnell", + "slug": "schnell" + }, + { + "id": "4c79c0b7-c2d0-415a-b5cf-138cfce92c7e", + "name": "Einfach", + "slug": "einfach" + }, + { + "id": "66bc0f60-ff95-44e4-afef-8437b2c2d9af", + "name": "Backen", + "slug": "backen" + }, + { + "id": "48d2a71c-ed17-4c07-bf9f-bc9216936f54", + "name": "Kuchen", + "slug": "kuchen" + }, + { + "id": "b2821b25-94ea-4576-b488-276331b3d76e", + "name": "Kinder", + "slug": "kinder" + }, + { + "id": "fee5e626-792c-479d-a265-81a0029047f2", + "name": "Mehlspeisen", + "slug": "mehlspeisen" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/1208161226570428/Der-perfekte-Pfannkuchen-gelingt-einfach-immer.html", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:06:40.503968", + "createdAt": "2024-01-21T04:04:43.296547", + "updateAt": "2024-01-21T04:06:40.506886", + "lastMade": null + }, + { + "id": "cf239441-b75d-4dea-a48e-9d99b7cb5842", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Dinkel-Sauerteigbrot", + "slug": "dinkel-sauerteigbrot", + "image": "yNDq", + "recipeYield": "1", + "totalTime": "24h", + "prepTime": "1h", + "cookTime": null, + "performTime": "35min", + "description": "Für alle Liebhaber von Dinkel ist dieses Dinkel-Sauerteigbrot ein absolutes Muss. Aussen knusprig und innen herrlich feucht und grossporig.", + "recipeCategory": [ + { + "id": "6d54ca14-eb71-4d3a-933d-5e88f68edb68", + "name": "Brot", + "slug": "brot" + } + ], + "tags": [ + { + "id": "0f80c5d5-d1ee-41ac-a949-54a76b446459", + "name": "Sourdough", + "slug": "sourdough" + } + ], + "tools": [ + { + "id": "1170e609-20d3-45b8-b0c7-3a4cfa614e88", + "name": "Backofen", + "slug": "backofen", + "onHand": false + } + ], + "rating": null, + "orgURL": "https://www.besondersgut.ch/dinkel-sauerteigbrot/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T03:57:41.588112", + "createdAt": "2024-01-21T03:44:30.512149", + "updateAt": "2024-01-21T03:44:30.512151", + "lastMade": null + }, + { + "id": "2673eb90-6d78-4b95-af36-5db8c8a6da37", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "test 234234", + "slug": "test-234234", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:07:55.643655", + "createdAt": "2024-01-21T03:14:59.852966", + "updateAt": "2024-01-21T04:07:55.646291", + "lastMade": null + }, + { + "id": "0a723c54-af53-40e9-a15f-c87aae5ac688", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "test 243", + "slug": "test-243", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T02:20:32.570339", + "createdAt": "2024-01-21T02:20:32.572744", + "updateAt": "2024-01-21T02:20:32.572746", + "lastMade": null + }, + { + "id": "9d553779-607e-471b-acf3-84e6be27b159", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Einfacher Nudelauflauf mit Brokkoli", + "slug": "einfacher-nudelauflauf-mit-brokkoli", + "image": "nOPT", + "recipeYield": "4 servings", + "totalTime": "35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T03:04:25.718367", + "createdAt": "2024-01-21T02:13:11.323363", + "updateAt": "2024-01-21T03:04:25.721489", + "lastMade": null + }, + { + "id": "9d3cb303-a996-4144-948a-36afaeeef554", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Tarta cytrynowa z bezą", + "slug": "tarta-cytrynowa-z-beza", + "image": "vxuL", + "recipeYield": "8 servings", + "totalTime": "1 Hour", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": null, + "description": "Tarta cytrynowa z bezą\r\nLekko kwaśna masa cytrynowa, która równoważy słodycz bezy – jeśli to brzmi jak ciasto, które chętnie zjesz na deser, wypróbuj nasz przepis! Tarta z bezą i masą cytrynową nawiązuje do kuchni francuskiej, znanej z wyśmienitych quiche i tart. Tym razem proponujemy ją w wersji na słodko.\r\nDla kogo?\r\nLubisz ciasta o delikatnym, kruchym spodzie? Posmakuje ci tarta cytrynowa z bezą. Przepis jest wprost stworzony dla miłośników lekko cierpkiego smaku cytrusów w wypiekach. Tarta cytrynowa z bezą zdecydowanie nie jest mdłym ciastem!\r\nNa jaką okazję?\r\nNa rodzinnym stole, zamiast zwykłego sernika lub ciasta czekoladowego, może stanąć właśnie tarta cytrynowa z bezą. Przepis ten skradnie serce twojej przyjaciółki lub przyjaciela, którego zaprosisz na herbatę i ciasto. Naszym zdaniem ma też dużą szansę stać się hitem urodzinowej imprezy, gdy pojawi się tuż obok tortu. Tarta cytrynowa z bezą smakuje doskonale w okresie świątecznym – upiecz ją na Wielkanoc oprócz tradycyjnego mazurka i baby.\r\nCzy wiesz, że?\r\nZastanawiasz się, czy kupione kilka dni temu cytryny możesz przeznaczyć do przepisu na tartę? Jest wiele sposobów na przedłużenie ich świeżości. Niektórzy trzymają je w lodówce, w torebce zamykanej strunowo. Ciekawostka: im mocniej pachnie cytryna, tym kwaśniejsza będzie w smaku.\r\nDla urozmaicenia:\r\nMartwisz się o to, czy każda warstwa tarty odpowiednio się upiecze? Mamy na to sposób. Piecz ją w piekarniku bez termoobiegu, ustawionym na grzanie góra–dół.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.przepisy.pl/przepis/tarta-cytrynowa-z-beza", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:27:12.082247", + "createdAt": "2024-01-21T01:27:12.088594", + "updateAt": "2024-01-21T01:27:12.088596", + "lastMade": null + }, + { + "id": "77f05a49-e869-4048-aa62-0d8a1f5a8f1c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Martins test Recipe", + "slug": "martins-test-recipe", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:26:38.793372", + "createdAt": "2024-01-21T01:26:38.802872", + "updateAt": "2024-01-21T01:26:38.802874", + "lastMade": null + }, + { + "id": "75a90207-9c10-4390-a265-c47a4b67fd69", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Muffinki czekoladowe", + "slug": "muffinki-czekoladowe", + "image": "xP1Q", + "recipeYield": "12", + "totalTime": null, + "prepTime": "25 Minutes", + "cookTime": null, + "performTime": "30 Minutes", + "description": "Muffinki czekoladowe to przepyszny i bardzo prosty w przygotowaniu mini deser pieczony w papilotkach. Przepis na najlepsze, bardzo wilgotne i puszyste muffinki czekoladowe polecam każdemu miłośnikowi czekolady.", + "recipeCategory": [], + "tags": [ + { + "id": "ed2eed99-1285-4507-b5cb-b3047d64855c", + "name": "Muffinki Czekoladowe", + "slug": "muffinki-czekoladowe" + }, + { + "id": "e94d5223-5337-4e1b-b36e-7968c8823176", + "name": "Babeczki I Muffiny", + "slug": "babeczki-i-muffiny" + }, + { + "id": "2d06a44a-331a-4922-abb4-8047ee5e7c1c", + "name": "Sylwester", + "slug": "sylwester" + }, + { + "id": "c78edd8c-c96b-43fb-86c0-917ea5a08ac7", + "name": "Wegetariańska", + "slug": "wegetarianska" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://aniagotuje.pl/przepis/muffinki-czekoladowe", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:25:53.529639", + "createdAt": "2024-01-21T01:25:03.838184", + "updateAt": "2024-01-21T01:25:53.534515", + "lastMade": null + }, + { + "id": "4320ba72-377b-4657-8297-dce198f24cdf", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "My Test Recipe", + "slug": "my-test-recipe", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:22:10.331488", + "createdAt": "2024-01-21T01:22:10.361617", + "updateAt": "2024-01-21T01:22:10.361618", + "lastMade": null + }, + { + "id": "98dac844-31ee-426a-b16c-fb62a5dd2816", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "My Test Receipe", + "slug": "my-test-receipe", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:22:10.309993", + "createdAt": "2024-01-21T01:22:10.357806", + "updateAt": "2024-01-21T01:22:10.357807", + "lastMade": null + }, + { + "id": "c3c8f207-c704-415d-81b1-da9f032cf52f", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Patates douces au four", + "slug": "patates-douces-au-four", + "image": "r1ck", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T00:34:57.419501", + "createdAt": "2024-01-21T00:34:57.422137", + "updateAt": "2024-01-21T00:34:57.422139", + "lastMade": null + }, + { + "id": "1edb2f6e-133c-4be0-b516-3c23625a97ec", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Easy Homemade Pizza Dough", + "slug": "easy-homemade-pizza-dough", + "image": "gD94", + "recipeYield": "2 servings", + "totalTime": "2 Hours 30 Minutes", + "prepTime": "2 Hours 15 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Follow these basic instructions for a thick, crisp, and chewy pizza crust at home. The recipe yields enough pizza dough for two 12-inch pizzas and you can freeze half of the dough for later. Close to 2 pounds of dough total.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://sallysbakingaddiction.com/homemade-pizza-crust-recipe/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T22:41:09.255367", + "createdAt": "2024-01-20T22:41:09.258070", + "updateAt": "2024-01-20T22:41:09.258071", + "lastMade": null + }, + { + "id": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "All-American Beef Stew Recipe", + "slug": "all-american-beef-stew-recipe", + "image": "356X", + "recipeYield": "6 servings", + "totalTime": "3 Hours 15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "3 Hours 10 Minutes", + "description": "This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.seriouseats.com/all-american-beef-stew-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T03:04:45.606075", + "createdAt": "2024-01-20T20:41:29.266390", + "updateAt": "2024-01-21T03:04:45.609563", + "lastMade": null + }, + { + "id": "6530ea6e-401e-4304-8a7a-12162ddf5b9c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Serious Eats' Halal Cart-Style Chicken and Rice With White Sauce", + "slug": "serious-eats-halal-cart-style-chicken-and-rice-with-white-sauce", + "image": "4Sys", + "recipeYield": "4 servings", + "totalTime": "2 Hours 15 Minutes", + "prepTime": "20 Minutes", + "cookTime": null, + "performTime": "55 Minutes", + "description": "This utterly faithful recipe perfectly recreates a New York City halal-cart classic: Chicken and Rice with White Sauce. The chicken is marinated with herbs, lemon, and spices; the rice golden; the sauce, as white and creamy as ever.", + "recipeCategory": [], + "tags": [ + { + "id": "d7aea128-0e7b-4e0c-a236-e500717701bb", + "name": "Rice", + "slug": "rice" + }, + { + "id": "1dd3541c-ed6b-4a25-b829-9a71358409ef", + "name": "Chicken", + "slug": "chicken" + }, + { + "id": "eb871b57-ea46-4cb5-88a5-98064514e593", + "name": "Chicken And Rice", + "slug": "chicken-and-rice" + }, + { + "id": "2b0a0ed2-e799-4ab2-8a24-d5ce15827a8e", + "name": "Cook The Book", + "slug": "cook-the-book" + }, + { + "id": "e6783087-0cee-4f31-b588-268380f75335", + "name": "Halal", + "slug": "halal" + }, + { + "id": "a2d99845-8bd0-4a2a-9a56-f8a34f51039e", + "name": "Middle Eastern", + "slug": "middle-eastern" + }, + { + "id": "6b7b95b0-b3f8-467f-857d-ef036009d5e1", + "name": "New York City", + "slug": "new-york-city" + }, + { + "id": "6bd6c577-9d00-411f-88de-b8679c37ac58", + "name": "Serious Eats Book", + "slug": "serious-eats-book" + }, + { + "id": "d77a2071-43ae-40b1-854d-ae995a766fba", + "name": "Street Food", + "slug": "street-food" + } + ], + "tools": [], + "rating": 5, + "orgURL": "https://www.seriouseats.com/serious-eats-halal-cart-style-chicken-and-rice-white-sauce-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T20:32:14.736668", + "createdAt": "2024-01-20T20:25:43.655397", + "updateAt": "2024-01-20T20:32:14.740947", + "lastMade": null + }, + { + "id": "c496cf9c-1ece-448a-9d3f-ef772f078a4e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Schnelle Käsespätzle", + "slug": "schnelle-kasespatzle", + "image": "8goY", + "recipeYield": "4 servings", + "totalTime": "40 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "30 Minutes", + "description": "Schnelle Käsespätzle. Über 1201 Bewertungen und für sehr gut befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/1062121211526182/Schnelle-Kaesespaetzle.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T18:31:51.652135", + "createdAt": "2024-01-20T18:31:51.654414", + "updateAt": "2024-01-20T18:31:51.654415", + "lastMade": null + }, + { + "id": "49aa6f42-6760-4adf-b6cd-59592da485c3", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "taco", + "slug": "taco", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T17:25:27.960087", + "createdAt": "2024-01-20T17:25:27.961639", + "updateAt": "2024-01-20T17:25:27.961641", + "lastMade": null + }, + { + "id": "6402a253-2baa-460d-bf4f-b759bb655588", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Vodkapasta", + "slug": "vodkapasta", + "image": "z8BB", + "recipeYield": "4 servings", + "totalTime": "30 Minutes", + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.ica.se/recept/vodkapasta-729011/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T01:58:25.398326", + "createdAt": "2024-01-20T15:35:35.492234", + "updateAt": "2024-01-21T01:58:25.400556", + "lastMade": "2024-01-21T22:59:59" + }, + { + "id": "4f54e9e1-f21d-40ec-a135-91e633dfb733", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Vodkapasta2", + "slug": "vodkapasta2", + "image": "Nqpz", + "recipeYield": "4 servings", + "totalTime": "30 Minutes", + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.ica.se/recept/vodkapasta-729011/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T17:35:32.077132", + "createdAt": "2024-01-20T15:35:35.492234", + "updateAt": "2024-01-20T17:24:19.620474", + "lastMade": "2024-01-21T04:59:59" + }, + { + "id": "e1a3edb0-49a0-49a3-83e3-95554e932670", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Rub", + "slug": "rub", + "image": null, + "recipeYield": "1", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:55:15.172744", + "createdAt": "2024-01-20T13:53:34.298477", + "updateAt": "2024-01-20T13:55:15.174780", + "lastMade": null + }, + { + "id": "1a0f4e54-db5b-40f1-ab7e-166dab5f6523", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Banana Bread Chocolate Chip Cookies", + "slug": "banana-bread-chocolate-chip-cookies", + "image": "03XS", + "recipeYield": "", + "totalTime": null, + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Tender and moist, these chocolate chip cookies were a HUGE hit in the Test Kitchen. They're like banana bread in a cookie form. Outside, there are crisp edges like a cookie. Inside, though, it's soft like banana bread. We opted to add chocolate chips and nuts. It's a classic flavor combination in banana bread and works just as well in these cookies.", + "recipeCategory": [], + "tags": [ + { + "id": "6a59e597-9aff-4716-961f-f236b93c34cc", + "name": "Cookies", + "slug": "cookies" + }, + { + "id": "1249f351-4b45-455d-b5f0-64eb0124a41e", + "name": "Banana", + "slug": "banana" + }, + { + "id": "81a446b9-4d8d-451d-a472-486987fad85a", + "name": "Bread", + "slug": "bread" + }, + { + "id": "c2536221-b1c3-4402-a104-46c632663748", + "name": "Chocolate Chip", + "slug": "chocolate-chip" + }, + { + "id": "c026c67f-0211-419f-9db8-7cd4c7608589", + "name": "Cookie", + "slug": "cookie" + }, + { + "id": "2f9e0bf5-02e2-4bdc-9b5d-a16d2fec885b", + "name": "American", + "slug": "american" + }, + { + "id": "2a7c5386-5d26-44fa-8a08-81747ee7f132", + "name": "Bake", + "slug": "bake" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.justapinch.com/recipes/dessert/cookies/banana-bread-chocolate-chip-cookies.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:52:21.817496", + "createdAt": "2024-01-20T13:51:46.727976", + "updateAt": "2024-01-20T13:52:21.821329", + "lastMade": null + }, + { + "id": "447acae6-3424-4c16-8c26-c09040ad8041", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Cauliflower Bisque Recipe with Cheddar Cheese", + "slug": "cauliflower-bisque-recipe-with-cheddar-cheese", + "image": "KuXV", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Hello friends, today I'm going to share with you how to make a delicious soup/bisque. A Cauliflower Bisques Recipe with Cheddar Cheese. One of my favorite soups to make when its cold outside. We will be continuing the soup collection so let me know what you think in the comments below!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://chefjeanpierre.com/recipes/soups/creamy-cauliflower-bisque/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:45:10.848270", + "createdAt": "2024-01-20T13:44:59.990057", + "updateAt": "2024-01-20T13:45:10.851647", + "lastMade": null + }, + { + "id": "864136a3-27b0-4f3b-a90f-486f42d6df7a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Prova ", + "slug": "prova", + "image": null, + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:44:41.788771", + "createdAt": "2024-01-20T13:42:56.178473", + "updateAt": "2024-01-20T13:42:56.178475", + "lastMade": null + }, + { + "id": "c7ccf4c7-c5f4-4191-a79b-1a49d068f6a4", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "pate au beurre (1)", + "slug": "pate-au-beurre-1", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:17:47.586659", + "createdAt": "2024-01-20T13:17:47.592852", + "updateAt": "2024-01-20T13:17:47.592854", + "lastMade": null + }, + { + "id": "d01865c3-0f18-4e8d-84c0-c14c345fdf9c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "pate au beurre", + "slug": "pate-au-beurre", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:16:49.702039", + "createdAt": "2024-01-20T13:16:49.704498", + "updateAt": "2024-01-20T13:16:49.704500", + "lastMade": null + }, + { + "id": "2cec2bb2-19b6-40b8-a36c-1a76ea29c517", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Sous Vide Cheesecake Recipe", + "slug": "sous-vide-cheesecake-recipe", + "image": "tmwm", + "recipeYield": "4 servings", + "totalTime": "2 Hours 10 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "1 Hour 30 Minutes", + "description": "Individual foolproof mason jar cheesecakes with strawberry compote and a Graham cracker crumble topping. Foolproof, simple, and delicious.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://saltpepperskillet.com/recipes/sous-vide-cheesecake/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:07:19.939939", + "createdAt": "2024-01-20T13:07:19.946260", + "updateAt": "2024-01-20T13:07:19.946263", + "lastMade": null + }, + { + "id": "8e0e4566-9caf-4c2e-a01c-dcead23db86b", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "The Bomb Mini Cheesecakes", + "slug": "the-bomb-mini-cheesecakes", + "image": "xCYc", + "recipeYield": "10 servings", + "totalTime": "1 Hour 30 Minutes", + "prepTime": "30 Minutes", + "cookTime": null, + "performTime": null, + "description": "This is a variation of the several cheese cake recipes that have been used for sous vide. These make a fabulous 4oz cheese cake for dessert. Garnish with a raspberry or blackberry and impress your family and friends. They’ll keep great in the fridge for a week easily.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://recipes.anovaculinary.com/recipe/the-bomb-cheesecakes", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:05:24.037000", + "createdAt": "2024-01-20T13:05:24.039558", + "updateAt": "2024-01-20T13:05:24.039560", + "lastMade": null + }, + { + "id": "a051eafd-9712-4aee-a8e5-0cd10a6772ee", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Tagliatelle al Salmone", + "slug": "tagliatelle-al-salmone", + "image": "qzaN", + "recipeYield": "4 servings", + "totalTime": "25 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Tagliatelle al Salmone - wie beim Italiener. Über 1568 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + "recipeCategory": [], + "tags": [ + { + "id": "518f3081-a919-4c80-9cad-75ffbd0e73d3", + "name": "Gemüse", + "slug": "gemuse" + }, + { + "id": "a3fff625-1902-4112-b169-54aec4f52ea7", + "name": "Hauptspeise", + "slug": "hauptspeise" + }, + { + "id": "4ec445c6-fc2f-4a1e-b666-93435a46ec42", + "name": "Schnell", + "slug": "schnell" + }, + { + "id": "4c79c0b7-c2d0-415a-b5cf-138cfce92c7e", + "name": "Einfach", + "slug": "einfach" + }, + { + "id": "6f349f84-655b-4740-8fa6-ed2716f17df7", + "name": "Gekocht", + "slug": "gekocht" + }, + { + "id": "77bc190f-dc6d-440b-aa82-f32bfe836018", + "name": "Europa", + "slug": "europa" + }, + { + "id": "7997c911-14ee-4e76-9895-debad7949ae2", + "name": "Pasta", + "slug": "pasta" + }, + { + "id": "04d2aea8-fc9a-4f9b-9a87-8f15189ab6f9", + "name": "Nudeln", + "slug": "nudeln" + }, + { + "id": "c56cd402-3ac7-479e-b96c-d4b64d177dd3", + "name": "Fisch", + "slug": "fisch" + }, + { + "id": "88015586-0885-4397-9098-039ae1109cd1", + "name": "Italien", + "slug": "italien" + }, + { + "id": "024b30ca-53cb-4243-ba6b-d830610f2f48", + "name": "Saucen", + "slug": "saucen" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/2109501340136606/Tagliatelle-al-Salmone.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:02:16.760030", + "createdAt": "2024-01-20T13:02:16.763188", + "updateAt": "2024-01-20T13:02:16.763189", + "lastMade": null + }, + { + "id": "093d51e9-0823-40ad-8e0e-a1d5790dd627", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Death by Chocolate", + "slug": "death-by-chocolate", + "image": "K9qP", + "recipeYield": "1 serving", + "totalTime": null, + "prepTime": "25 Minutes", + "cookTime": null, + "performTime": "25 Minutes", + "description": "Hier ist der Name Programm: Den \"Tod durch Schokolade\" müsst ihr zwar hoffentlich nicht erleiden, aber Chocoholics werden diesen Kuchen lieben!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.backenmachtgluecklich.de/rezepte/death-by-chocolate-kuchen.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T12:58:50.926224", + "createdAt": "2024-01-20T12:58:50.928810", + "updateAt": "2024-01-20T12:58:50.928812", + "lastMade": null + }, + { + "id": "2d1f62ec-4200-4cfd-987e-c75755d7607c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Palak Dal Rezept aus Indien", + "slug": "palak-dal-rezept-aus-indien", + "image": "jKQ3", + "recipeYield": "4 servings", + "totalTime": "30 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Palak Dal ist in Grunde genommen Spinat (Palak) mit Linsen oder anderen Hülsenfrüchten (Dal) vom indischen Subkontinent. Es kommen noch Zwiebeln, Tomaten und einige indische Gewürze dazu. Damit ist das Palak Dal ein super einfaches und zugleich veganes indisches Rezept. Es schmeckt hervorragend mit Naan-Brot und etwas gewürztem Joghurt.", + "recipeCategory": [], + "tags": [ + { + "id": "38d18d57-d817-491e-94f8-da923d2c540e", + "name": "Eintopf", + "slug": "eintopf" + }, + { + "id": "43f12acf-a8df-45bd-b33d-20bfe7a7e607", + "name": "Indisch", + "slug": "indisch" + }, + { + "id": "ede834ac-ab8f-4c79-8a42-dfa0270fd18b", + "name": "Linsen", + "slug": "linsen" + }, + { + "id": "2b6283e2-b8e0-4b3d-90d9-66f322ca77aa", + "name": "Spinat", + "slug": "spinat" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.fernweh-koch.de/palak-dal-indischer-spinat-linsen-rezept/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T12:46:54.570376", + "createdAt": "2024-01-20T12:46:54.573341", + "updateAt": "2024-01-20T12:46:54.573342", + "lastMade": null + }, + { + "id": "973dc36d-1661-49b4-ad2d-0b7191034fb3", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Tortelline - á la Romana", + "slug": "tortelline-a-la-romana", + "image": "rkSn", + "recipeYield": "4 servings", + "totalTime": "30 Minutes", + "prepTime": "30 Minutes", + "cookTime": null, + "performTime": null, + "description": "Tortelline - á la Romana. Über 13 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!", + "recipeCategory": [], + "tags": [ + { + "id": "4c79c0b7-c2d0-415a-b5cf-138cfce92c7e", + "name": "Einfach", + "slug": "einfach" + }, + { + "id": "7997c911-14ee-4e76-9895-debad7949ae2", + "name": "Pasta", + "slug": "pasta" + }, + { + "id": "04d2aea8-fc9a-4f9b-9a87-8f15189ab6f9", + "name": "Nudeln", + "slug": "nudeln" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/74441028021809/Tortelline-a-la-Romana.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:44:42.215472", + "createdAt": "2024-01-20T12:29:47.825708", + "updateAt": "2024-01-20T13:44:42.218635", + "lastMade": "2024-01-21T20:59:59" + } + ], + "next": "/recipes?page=2&perPage=50&orderDirection=desc", + "previous": null +} diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 56626c7b5c4..257d685d8dc 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -1,4 +1,1242 @@ # serializer version: 1 +# name: test_service_get_recipes[service_data0] + dict({ + 'recipes': dict({ + 'items': list([ + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'tu6y', + 'original_url': None, + 'recipe_id': 'e82f5449-c33b-437c-b712-337587199264', + 'recipe_yield': None, + 'slug': 'tu6y', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'En9o', + 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)', + 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', + 'recipe_id': 'f79f7e9d-4b58-4930-a586-2b127f16ee34', + 'recipe_yield': '6 servings', + 'slug': 'eukole-makaronada-me-kephtedakia-ston-phourno-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'aAhk', + 'name': 'Patates douces au four (1)', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': '90097c8b-9d80-468a-b497-73957ac0cd8b', + 'recipe_yield': '', + 'slug': 'patates-douces-au-four-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kdhm', + 'name': 'Sweet potatoes', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': '98845807-9365-41fd-acd1-35630b468c27', + 'recipe_yield': '', + 'slug': 'sweet-potatoes', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'tNbG', + 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο', + 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', + 'recipe_id': '40c227e0-3c7e-41f7-866d-5de04eaecdd7', + 'recipe_yield': '6 servings', + 'slug': 'eukole-makaronada-me-kephtedakia-ston-phourno', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'nj5M', + 'name': 'Boeuf bourguignon : la vraie recette (2)', + 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', + 'recipe_id': '9c7b8aee-c93c-4b1b-ab48-2625d444743a', + 'recipe_yield': '4 servings', + 'slug': 'boeuf-bourguignon-la-vraie-recette-2', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'rbU7', + 'name': 'Boeuf bourguignon : la vraie recette (1)', + 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', + 'recipe_id': 'fc42c7d1-7b0f-4e04-b88a-dbd80b81540b', + 'recipe_yield': '4 servings', + 'slug': 'boeuf-bourguignon-la-vraie-recette-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Dieses einfache vegane Erdnussbutter-Schoko-Marmor-Bananenbrot Rezept enthält kein Öl und keinen raffiniernten Zucker, ist aber so fluffig, weich, saftig und lecker wie ein Kuchen! Zubereitet mit vielen gesunden Bananen, gelingt es auch glutenfrei und eignet sich perfekt zum Frühstück, als Dessert oder Snack für Zwischendurch!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'JSp3', + 'name': 'Veganes Marmor-Bananenbrot mit Erdnussbutter', + 'original_url': 'https://biancazapatka.com/de/erdnussbutter-schoko-bananenbrot/', + 'recipe_id': '89e63d72-7a51-4cef-b162-2e45035d0a91', + 'recipe_yield': '14 servings', + 'slug': 'veganes-marmor-bananenbrot-mit-erdnussbutter', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Es ist kein Geheimnis: Ich mag es gerne schnell und einfach. Und ich liebe Pasta! Deshalb habe ich mich vor ein paar Wochen auf die Suche nach der perfekten, schnellen Tomatensoße gemacht. Es muss da draußen doch irgendein Rezept geben, das (fast) genauso schnell zuzubereiten ist, wie Miracoli und dabei aber das schöne Gefühl hinterlässt, ...', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '9QMh', + 'name': 'Pasta mit Tomaten, Knoblauch und Basilikum - einfach (und) genial! - Kuechenchaotin', + 'original_url': 'https://kuechenchaotin.de/pasta-mit-tomaten-knoblauch-basilikum/', + 'recipe_id': 'eab64457-97ba-4d6c-871c-cb1c724ccb51', + 'recipe_yield': '', + 'slug': 'pasta-mit-tomaten-knoblauch-und-basilikum-einfach-und-genial-kuechenchaotin', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test123', + 'original_url': None, + 'recipe_id': '12439e3d-3c1c-4dcc-9c6e-4afcea2a0542', + 'recipe_yield': None, + 'slug': 'test123', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Bureeto', + 'original_url': None, + 'recipe_id': '6567f6ec-e410-49cb-a1a5-d08517184e78', + 'recipe_yield': None, + 'slug': 'bureeto', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Subway Double Cookies', + 'original_url': None, + 'recipe_id': 'f7737d17-161c-4008-88d4-dd2616778cd0', + 'recipe_yield': None, + 'slug': 'subway-double-cookies', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'qwerty12345', + 'original_url': None, + 'recipe_id': '1904b717-4a8b-4de9-8909-56958875b5f4', + 'recipe_yield': None, + 'slug': 'qwerty12345', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'beGq', + 'name': 'Cheeseburger Sliders (Easy, 30-min Recipe)', + 'original_url': 'https://natashaskitchen.com/cheeseburger-sliders/', + 'recipe_id': '8bdd3656-5e7e-45d3-a3c4-557390846a22', + 'recipe_yield': '24 servings', + 'slug': 'cheeseburger-sliders-easy-30-min-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'meatloaf', + 'original_url': None, + 'recipe_id': '8a30d31d-aa14-411e-af0c-6b61a94f5291', + 'recipe_yield': '4', + 'slug': 'meatloaf', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Richtig rheinischer Sauerbraten - Rheinischer geht's nicht! Über 536 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kCBh', + 'name': 'Richtig rheinischer Sauerbraten', + 'original_url': 'https://www.chefkoch.de/rezepte/937641199437984/Richtig-rheinischer-Sauerbraten.html', + 'recipe_id': 'f2f7880b-1136-436f-91b7-129788d8c117', + 'recipe_yield': '4 servings', + 'slug': 'richtig-rheinischer-sauerbraten', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Orientalischer Gemüse-Hähnchen Eintopf. Über 164 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kpBx', + 'name': 'Orientalischer Gemüse-Hähnchen Eintopf', + 'original_url': 'https://www.chefkoch.de/rezepte/2307761368177614/Orientalischer-Gemuese-Haehnchen-Eintopf.html', + 'recipe_id': 'cf634591-0f82-4254-8e00-2f7e8b0c9022', + 'recipe_yield': '6 servings', + 'slug': 'orientalischer-gemuse-hahnchen-eintopf', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 20240121', + 'original_url': None, + 'recipe_id': '05208856-d273-4cc9-bcfa-e0215d57108d', + 'recipe_yield': '4', + 'slug': 'test-20240121', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Zet in 20 minuten deze lekkere loempia bowl in elkaar. Makkelijk, snel en weer eens wat anders. Lekker met prei, sojasaus en kipgehakt.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'McEx', + 'name': 'Loempia bowl', + 'original_url': 'https://www.lekkerensimpel.com/loempia-bowl/', + 'recipe_id': '145eeb05-781a-4eb0-a656-afa8bc8c0164', + 'recipe_yield': '', + 'slug': 'loempia-bowl', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Chocolate Mousse with Aquafaba, to make the fluffiest of mousses. Whip up this dessert in literally five minutes and chill in the fridge until you're ready to serve!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'bzqo', + 'name': '5 Ingredient Chocolate Mousse', + 'original_url': 'https://thehappypear.ie/aquafaba-chocolate-mousse/', + 'recipe_id': '5c6532aa-ad84-424c-bc05-c32d50430fe4', + 'recipe_yield': '6 servings', + 'slug': '5-ingredient-chocolate-mousse', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Der perfekte Pfannkuchen - gelingt einfach immer - von Kindern geliebt und auch für Kochneulinge super geeignet. Über 2529 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'KGK6', + 'name': 'Der perfekte Pfannkuchen - gelingt einfach immer', + 'original_url': 'https://www.chefkoch.de/rezepte/1208161226570428/Der-perfekte-Pfannkuchen-gelingt-einfach-immer.html', + 'recipe_id': 'f2e684f2-49e0-45ee-90de-951344472f1c', + 'recipe_yield': '4 servings', + 'slug': 'der-perfekte-pfannkuchen-gelingt-einfach-immer', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Für alle Liebhaber von Dinkel ist dieses Dinkel-Sauerteigbrot ein absolutes Muss. Aussen knusprig und innen herrlich feucht und grossporig.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'yNDq', + 'name': 'Dinkel-Sauerteigbrot', + 'original_url': 'https://www.besondersgut.ch/dinkel-sauerteigbrot/', + 'recipe_id': 'cf239441-b75d-4dea-a48e-9d99b7cb5842', + 'recipe_yield': '1', + 'slug': 'dinkel-sauerteigbrot', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 234234', + 'original_url': None, + 'recipe_id': '2673eb90-6d78-4b95-af36-5db8c8a6da37', + 'recipe_yield': None, + 'slug': 'test-234234', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 243', + 'original_url': None, + 'recipe_id': '0a723c54-af53-40e9-a15f-c87aae5ac688', + 'recipe_yield': None, + 'slug': 'test-243', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'nOPT', + 'name': 'Einfacher Nudelauflauf mit Brokkoli', + 'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/', + 'recipe_id': '9d553779-607e-471b-acf3-84e6be27b159', + 'recipe_yield': '4 servings', + 'slug': 'einfacher-nudelauflauf-mit-brokkoli', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': ''' + Tarta cytrynowa z bezą + Lekko kwaśna masa cytrynowa, która równoważy słodycz bezy – jeśli to brzmi jak ciasto, które chętnie zjesz na deser, wypróbuj nasz przepis! Tarta z bezą i masą cytrynową nawiązuje do kuchni francuskiej, znanej z wyśmienitych quiche i tart. Tym razem proponujemy ją w wersji na słodko. + Dla kogo? + Lubisz ciasta o delikatnym, kruchym spodzie? Posmakuje ci tarta cytrynowa z bezą. Przepis jest wprost stworzony dla miłośników lekko cierpkiego smaku cytrusów w wypiekach. Tarta cytrynowa z bezą zdecydowanie nie jest mdłym ciastem! + Na jaką okazję? + Na rodzinnym stole, zamiast zwykłego sernika lub ciasta czekoladowego, może stanąć właśnie tarta cytrynowa z bezą. Przepis ten skradnie serce twojej przyjaciółki lub przyjaciela, którego zaprosisz na herbatę i ciasto. Naszym zdaniem ma też dużą szansę stać się hitem urodzinowej imprezy, gdy pojawi się tuż obok tortu. Tarta cytrynowa z bezą smakuje doskonale w okresie świątecznym – upiecz ją na Wielkanoc oprócz tradycyjnego mazurka i baby. + Czy wiesz, że? + Zastanawiasz się, czy kupione kilka dni temu cytryny możesz przeznaczyć do przepisu na tartę? Jest wiele sposobów na przedłużenie ich świeżości. Niektórzy trzymają je w lodówce, w torebce zamykanej strunowo. Ciekawostka: im mocniej pachnie cytryna, tym kwaśniejsza będzie w smaku. + Dla urozmaicenia: + Martwisz się o to, czy każda warstwa tarty odpowiednio się upiecze? Mamy na to sposób. Piecz ją w piekarniku bez termoobiegu, ustawionym na grzanie góra–dół. + ''', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'vxuL', + 'name': 'Tarta cytrynowa z bezą', + 'original_url': 'https://www.przepisy.pl/przepis/tarta-cytrynowa-z-beza', + 'recipe_id': '9d3cb303-a996-4144-948a-36afaeeef554', + 'recipe_yield': '8 servings', + 'slug': 'tarta-cytrynowa-z-beza', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Martins test Recipe', + 'original_url': None, + 'recipe_id': '77f05a49-e869-4048-aa62-0d8a1f5a8f1c', + 'recipe_yield': None, + 'slug': 'martins-test-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Muffinki czekoladowe to przepyszny i bardzo prosty w przygotowaniu mini deser pieczony w papilotkach. Przepis na najlepsze, bardzo wilgotne i puszyste muffinki czekoladowe polecam każdemu miłośnikowi czekolady.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'xP1Q', + 'name': 'Muffinki czekoladowe', + 'original_url': 'https://aniagotuje.pl/przepis/muffinki-czekoladowe', + 'recipe_id': '75a90207-9c10-4390-a265-c47a4b67fd69', + 'recipe_yield': '12', + 'slug': 'muffinki-czekoladowe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'My Test Recipe', + 'original_url': None, + 'recipe_id': '4320ba72-377b-4657-8297-dce198f24cdf', + 'recipe_yield': None, + 'slug': 'my-test-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'My Test Receipe', + 'original_url': None, + 'recipe_id': '98dac844-31ee-426a-b16c-fb62a5dd2816', + 'recipe_yield': None, + 'slug': 'my-test-receipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'r1ck', + 'name': 'Patates douces au four', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': 'c3c8f207-c704-415d-81b1-da9f032cf52f', + 'recipe_yield': '', + 'slug': 'patates-douces-au-four', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Follow these basic instructions for a thick, crisp, and chewy pizza crust at home. The recipe yields enough pizza dough for two 12-inch pizzas and you can freeze half of the dough for later. Close to 2 pounds of dough total.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'gD94', + 'name': 'Easy Homemade Pizza Dough', + 'original_url': 'https://sallysbakingaddiction.com/homemade-pizza-crust-recipe/', + 'recipe_id': '1edb2f6e-133c-4be0-b516-3c23625a97ec', + 'recipe_yield': '2 servings', + 'slug': 'easy-homemade-pizza-dough', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '356X', + 'name': 'All-American Beef Stew Recipe', + 'original_url': 'https://www.seriouseats.com/all-american-beef-stew-recipe', + 'recipe_id': '48f39d27-4b8e-4c14-bf36-4e1e6497e75e', + 'recipe_yield': '6 servings', + 'slug': 'all-american-beef-stew-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This utterly faithful recipe perfectly recreates a New York City halal-cart classic: Chicken and Rice with White Sauce. The chicken is marinated with herbs, lemon, and spices; the rice golden; the sauce, as white and creamy as ever.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '4Sys', + 'name': "Serious Eats' Halal Cart-Style Chicken and Rice With White Sauce", + 'original_url': 'https://www.seriouseats.com/serious-eats-halal-cart-style-chicken-and-rice-white-sauce-recipe', + 'recipe_id': '6530ea6e-401e-4304-8a7a-12162ddf5b9c', + 'recipe_yield': '4 servings', + 'slug': 'serious-eats-halal-cart-style-chicken-and-rice-with-white-sauce', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Schnelle Käsespätzle. Über 1201 Bewertungen und für sehr gut befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '8goY', + 'name': 'Schnelle Käsespätzle', + 'original_url': 'https://www.chefkoch.de/rezepte/1062121211526182/Schnelle-Kaesespaetzle.html', + 'recipe_id': 'c496cf9c-1ece-448a-9d3f-ef772f078a4e', + 'recipe_yield': '4 servings', + 'slug': 'schnelle-kasespatzle', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'taco', + 'original_url': None, + 'recipe_id': '49aa6f42-6760-4adf-b6cd-59592da485c3', + 'recipe_yield': None, + 'slug': 'taco', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'z8BB', + 'name': 'Vodkapasta', + 'original_url': 'https://www.ica.se/recept/vodkapasta-729011/', + 'recipe_id': '6402a253-2baa-460d-bf4f-b759bb655588', + 'recipe_yield': '4 servings', + 'slug': 'vodkapasta', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'Nqpz', + 'name': 'Vodkapasta2', + 'original_url': 'https://www.ica.se/recept/vodkapasta-729011/', + 'recipe_id': '4f54e9e1-f21d-40ec-a135-91e633dfb733', + 'recipe_yield': '4 servings', + 'slug': 'vodkapasta2', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Rub', + 'original_url': None, + 'recipe_id': 'e1a3edb0-49a0-49a3-83e3-95554e932670', + 'recipe_yield': '1', + 'slug': 'rub', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Tender and moist, these chocolate chip cookies were a HUGE hit in the Test Kitchen. They're like banana bread in a cookie form. Outside, there are crisp edges like a cookie. Inside, though, it's soft like banana bread. We opted to add chocolate chips and nuts. It's a classic flavor combination in banana bread and works just as well in these cookies.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '03XS', + 'name': 'Banana Bread Chocolate Chip Cookies', + 'original_url': 'https://www.justapinch.com/recipes/dessert/cookies/banana-bread-chocolate-chip-cookies.html', + 'recipe_id': '1a0f4e54-db5b-40f1-ab7e-166dab5f6523', + 'recipe_yield': '', + 'slug': 'banana-bread-chocolate-chip-cookies', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Hello friends, today I'm going to share with you how to make a delicious soup/bisque. A Cauliflower Bisques Recipe with Cheddar Cheese. One of my favorite soups to make when its cold outside. We will be continuing the soup collection so let me know what you think in the comments below!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'KuXV', + 'name': 'Cauliflower Bisque Recipe with Cheddar Cheese', + 'original_url': 'https://chefjeanpierre.com/recipes/soups/creamy-cauliflower-bisque/', + 'recipe_id': '447acae6-3424-4c16-8c26-c09040ad8041', + 'recipe_yield': '', + 'slug': 'cauliflower-bisque-recipe-with-cheddar-cheese', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Prova ', + 'original_url': None, + 'recipe_id': '864136a3-27b0-4f3b-a90f-486f42d6df7a', + 'recipe_yield': '', + 'slug': 'prova', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'pate au beurre (1)', + 'original_url': None, + 'recipe_id': 'c7ccf4c7-c5f4-4191-a79b-1a49d068f6a4', + 'recipe_yield': None, + 'slug': 'pate-au-beurre-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'pate au beurre', + 'original_url': None, + 'recipe_id': 'd01865c3-0f18-4e8d-84c0-c14c345fdf9c', + 'recipe_yield': None, + 'slug': 'pate-au-beurre', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Individual foolproof mason jar cheesecakes with strawberry compote and a Graham cracker crumble topping. Foolproof, simple, and delicious.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'tmwm', + 'name': 'Sous Vide Cheesecake Recipe', + 'original_url': 'https://saltpepperskillet.com/recipes/sous-vide-cheesecake/', + 'recipe_id': '2cec2bb2-19b6-40b8-a36c-1a76ea29c517', + 'recipe_yield': '4 servings', + 'slug': 'sous-vide-cheesecake-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This is a variation of the several cheese cake recipes that have been used for sous vide. These make a fabulous 4oz cheese cake for dessert. Garnish with a raspberry or blackberry and impress your family and friends. They’ll keep great in the fridge for a week easily.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'xCYc', + 'name': 'The Bomb Mini Cheesecakes', + 'original_url': 'https://recipes.anovaculinary.com/recipe/the-bomb-cheesecakes', + 'recipe_id': '8e0e4566-9caf-4c2e-a01c-dcead23db86b', + 'recipe_yield': '10 servings', + 'slug': 'the-bomb-mini-cheesecakes', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Tagliatelle al Salmone - wie beim Italiener. Über 1568 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'qzaN', + 'name': 'Tagliatelle al Salmone', + 'original_url': 'https://www.chefkoch.de/rezepte/2109501340136606/Tagliatelle-al-Salmone.html', + 'recipe_id': 'a051eafd-9712-4aee-a8e5-0cd10a6772ee', + 'recipe_yield': '4 servings', + 'slug': 'tagliatelle-al-salmone', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Hier ist der Name Programm: Den "Tod durch Schokolade" müsst ihr zwar hoffentlich nicht erleiden, aber Chocoholics werden diesen Kuchen lieben!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'K9qP', + 'name': 'Death by Chocolate', + 'original_url': 'https://www.backenmachtgluecklich.de/rezepte/death-by-chocolate-kuchen.html', + 'recipe_id': '093d51e9-0823-40ad-8e0e-a1d5790dd627', + 'recipe_yield': '1 serving', + 'slug': 'death-by-chocolate', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Palak Dal ist in Grunde genommen Spinat (Palak) mit Linsen oder anderen Hülsenfrüchten (Dal) vom indischen Subkontinent. Es kommen noch Zwiebeln, Tomaten und einige indische Gewürze dazu. Damit ist das Palak Dal ein super einfaches und zugleich veganes indisches Rezept. Es schmeckt hervorragend mit Naan-Brot und etwas gewürztem Joghurt.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'jKQ3', + 'name': 'Palak Dal Rezept aus Indien', + 'original_url': 'https://www.fernweh-koch.de/palak-dal-indischer-spinat-linsen-rezept/', + 'recipe_id': '2d1f62ec-4200-4cfd-987e-c75755d7607c', + 'recipe_yield': '4 servings', + 'slug': 'palak-dal-rezept-aus-indien', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Tortelline - á la Romana. Über 13 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'rkSn', + 'name': 'Tortelline - á la Romana', + 'original_url': 'https://www.chefkoch.de/rezepte/74441028021809/Tortelline-a-la-Romana.html', + 'recipe_id': '973dc36d-1661-49b4-ad2d-0b7191034fb3', + 'recipe_yield': '4 servings', + 'slug': 'tortelline-a-la-romana', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + ]), + }), + }) +# --- +# name: test_service_get_recipes[service_data1] + dict({ + 'recipes': dict({ + 'items': list([ + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'tu6y', + 'original_url': None, + 'recipe_id': 'e82f5449-c33b-437c-b712-337587199264', + 'recipe_yield': None, + 'slug': 'tu6y', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'En9o', + 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)', + 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', + 'recipe_id': 'f79f7e9d-4b58-4930-a586-2b127f16ee34', + 'recipe_yield': '6 servings', + 'slug': 'eukole-makaronada-me-kephtedakia-ston-phourno-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'aAhk', + 'name': 'Patates douces au four (1)', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': '90097c8b-9d80-468a-b497-73957ac0cd8b', + 'recipe_yield': '', + 'slug': 'patates-douces-au-four-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kdhm', + 'name': 'Sweet potatoes', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': '98845807-9365-41fd-acd1-35630b468c27', + 'recipe_yield': '', + 'slug': 'sweet-potatoes', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'tNbG', + 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο', + 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', + 'recipe_id': '40c227e0-3c7e-41f7-866d-5de04eaecdd7', + 'recipe_yield': '6 servings', + 'slug': 'eukole-makaronada-me-kephtedakia-ston-phourno', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'nj5M', + 'name': 'Boeuf bourguignon : la vraie recette (2)', + 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', + 'recipe_id': '9c7b8aee-c93c-4b1b-ab48-2625d444743a', + 'recipe_yield': '4 servings', + 'slug': 'boeuf-bourguignon-la-vraie-recette-2', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'rbU7', + 'name': 'Boeuf bourguignon : la vraie recette (1)', + 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', + 'recipe_id': 'fc42c7d1-7b0f-4e04-b88a-dbd80b81540b', + 'recipe_yield': '4 servings', + 'slug': 'boeuf-bourguignon-la-vraie-recette-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Dieses einfache vegane Erdnussbutter-Schoko-Marmor-Bananenbrot Rezept enthält kein Öl und keinen raffiniernten Zucker, ist aber so fluffig, weich, saftig und lecker wie ein Kuchen! Zubereitet mit vielen gesunden Bananen, gelingt es auch glutenfrei und eignet sich perfekt zum Frühstück, als Dessert oder Snack für Zwischendurch!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'JSp3', + 'name': 'Veganes Marmor-Bananenbrot mit Erdnussbutter', + 'original_url': 'https://biancazapatka.com/de/erdnussbutter-schoko-bananenbrot/', + 'recipe_id': '89e63d72-7a51-4cef-b162-2e45035d0a91', + 'recipe_yield': '14 servings', + 'slug': 'veganes-marmor-bananenbrot-mit-erdnussbutter', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Es ist kein Geheimnis: Ich mag es gerne schnell und einfach. Und ich liebe Pasta! Deshalb habe ich mich vor ein paar Wochen auf die Suche nach der perfekten, schnellen Tomatensoße gemacht. Es muss da draußen doch irgendein Rezept geben, das (fast) genauso schnell zuzubereiten ist, wie Miracoli und dabei aber das schöne Gefühl hinterlässt, ...', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '9QMh', + 'name': 'Pasta mit Tomaten, Knoblauch und Basilikum - einfach (und) genial! - Kuechenchaotin', + 'original_url': 'https://kuechenchaotin.de/pasta-mit-tomaten-knoblauch-basilikum/', + 'recipe_id': 'eab64457-97ba-4d6c-871c-cb1c724ccb51', + 'recipe_yield': '', + 'slug': 'pasta-mit-tomaten-knoblauch-und-basilikum-einfach-und-genial-kuechenchaotin', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test123', + 'original_url': None, + 'recipe_id': '12439e3d-3c1c-4dcc-9c6e-4afcea2a0542', + 'recipe_yield': None, + 'slug': 'test123', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Bureeto', + 'original_url': None, + 'recipe_id': '6567f6ec-e410-49cb-a1a5-d08517184e78', + 'recipe_yield': None, + 'slug': 'bureeto', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Subway Double Cookies', + 'original_url': None, + 'recipe_id': 'f7737d17-161c-4008-88d4-dd2616778cd0', + 'recipe_yield': None, + 'slug': 'subway-double-cookies', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'qwerty12345', + 'original_url': None, + 'recipe_id': '1904b717-4a8b-4de9-8909-56958875b5f4', + 'recipe_yield': None, + 'slug': 'qwerty12345', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'beGq', + 'name': 'Cheeseburger Sliders (Easy, 30-min Recipe)', + 'original_url': 'https://natashaskitchen.com/cheeseburger-sliders/', + 'recipe_id': '8bdd3656-5e7e-45d3-a3c4-557390846a22', + 'recipe_yield': '24 servings', + 'slug': 'cheeseburger-sliders-easy-30-min-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'meatloaf', + 'original_url': None, + 'recipe_id': '8a30d31d-aa14-411e-af0c-6b61a94f5291', + 'recipe_yield': '4', + 'slug': 'meatloaf', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Richtig rheinischer Sauerbraten - Rheinischer geht's nicht! Über 536 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kCBh', + 'name': 'Richtig rheinischer Sauerbraten', + 'original_url': 'https://www.chefkoch.de/rezepte/937641199437984/Richtig-rheinischer-Sauerbraten.html', + 'recipe_id': 'f2f7880b-1136-436f-91b7-129788d8c117', + 'recipe_yield': '4 servings', + 'slug': 'richtig-rheinischer-sauerbraten', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Orientalischer Gemüse-Hähnchen Eintopf. Über 164 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kpBx', + 'name': 'Orientalischer Gemüse-Hähnchen Eintopf', + 'original_url': 'https://www.chefkoch.de/rezepte/2307761368177614/Orientalischer-Gemuese-Haehnchen-Eintopf.html', + 'recipe_id': 'cf634591-0f82-4254-8e00-2f7e8b0c9022', + 'recipe_yield': '6 servings', + 'slug': 'orientalischer-gemuse-hahnchen-eintopf', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 20240121', + 'original_url': None, + 'recipe_id': '05208856-d273-4cc9-bcfa-e0215d57108d', + 'recipe_yield': '4', + 'slug': 'test-20240121', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Zet in 20 minuten deze lekkere loempia bowl in elkaar. Makkelijk, snel en weer eens wat anders. Lekker met prei, sojasaus en kipgehakt.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'McEx', + 'name': 'Loempia bowl', + 'original_url': 'https://www.lekkerensimpel.com/loempia-bowl/', + 'recipe_id': '145eeb05-781a-4eb0-a656-afa8bc8c0164', + 'recipe_yield': '', + 'slug': 'loempia-bowl', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Chocolate Mousse with Aquafaba, to make the fluffiest of mousses. Whip up this dessert in literally five minutes and chill in the fridge until you're ready to serve!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'bzqo', + 'name': '5 Ingredient Chocolate Mousse', + 'original_url': 'https://thehappypear.ie/aquafaba-chocolate-mousse/', + 'recipe_id': '5c6532aa-ad84-424c-bc05-c32d50430fe4', + 'recipe_yield': '6 servings', + 'slug': '5-ingredient-chocolate-mousse', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Der perfekte Pfannkuchen - gelingt einfach immer - von Kindern geliebt und auch für Kochneulinge super geeignet. Über 2529 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'KGK6', + 'name': 'Der perfekte Pfannkuchen - gelingt einfach immer', + 'original_url': 'https://www.chefkoch.de/rezepte/1208161226570428/Der-perfekte-Pfannkuchen-gelingt-einfach-immer.html', + 'recipe_id': 'f2e684f2-49e0-45ee-90de-951344472f1c', + 'recipe_yield': '4 servings', + 'slug': 'der-perfekte-pfannkuchen-gelingt-einfach-immer', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Für alle Liebhaber von Dinkel ist dieses Dinkel-Sauerteigbrot ein absolutes Muss. Aussen knusprig und innen herrlich feucht und grossporig.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'yNDq', + 'name': 'Dinkel-Sauerteigbrot', + 'original_url': 'https://www.besondersgut.ch/dinkel-sauerteigbrot/', + 'recipe_id': 'cf239441-b75d-4dea-a48e-9d99b7cb5842', + 'recipe_yield': '1', + 'slug': 'dinkel-sauerteigbrot', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 234234', + 'original_url': None, + 'recipe_id': '2673eb90-6d78-4b95-af36-5db8c8a6da37', + 'recipe_yield': None, + 'slug': 'test-234234', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 243', + 'original_url': None, + 'recipe_id': '0a723c54-af53-40e9-a15f-c87aae5ac688', + 'recipe_yield': None, + 'slug': 'test-243', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'nOPT', + 'name': 'Einfacher Nudelauflauf mit Brokkoli', + 'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/', + 'recipe_id': '9d553779-607e-471b-acf3-84e6be27b159', + 'recipe_yield': '4 servings', + 'slug': 'einfacher-nudelauflauf-mit-brokkoli', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': ''' + Tarta cytrynowa z bezą + Lekko kwaśna masa cytrynowa, która równoważy słodycz bezy – jeśli to brzmi jak ciasto, które chętnie zjesz na deser, wypróbuj nasz przepis! Tarta z bezą i masą cytrynową nawiązuje do kuchni francuskiej, znanej z wyśmienitych quiche i tart. Tym razem proponujemy ją w wersji na słodko. + Dla kogo? + Lubisz ciasta o delikatnym, kruchym spodzie? Posmakuje ci tarta cytrynowa z bezą. Przepis jest wprost stworzony dla miłośników lekko cierpkiego smaku cytrusów w wypiekach. Tarta cytrynowa z bezą zdecydowanie nie jest mdłym ciastem! + Na jaką okazję? + Na rodzinnym stole, zamiast zwykłego sernika lub ciasta czekoladowego, może stanąć właśnie tarta cytrynowa z bezą. Przepis ten skradnie serce twojej przyjaciółki lub przyjaciela, którego zaprosisz na herbatę i ciasto. Naszym zdaniem ma też dużą szansę stać się hitem urodzinowej imprezy, gdy pojawi się tuż obok tortu. Tarta cytrynowa z bezą smakuje doskonale w okresie świątecznym – upiecz ją na Wielkanoc oprócz tradycyjnego mazurka i baby. + Czy wiesz, że? + Zastanawiasz się, czy kupione kilka dni temu cytryny możesz przeznaczyć do przepisu na tartę? Jest wiele sposobów na przedłużenie ich świeżości. Niektórzy trzymają je w lodówce, w torebce zamykanej strunowo. Ciekawostka: im mocniej pachnie cytryna, tym kwaśniejsza będzie w smaku. + Dla urozmaicenia: + Martwisz się o to, czy każda warstwa tarty odpowiednio się upiecze? Mamy na to sposób. Piecz ją w piekarniku bez termoobiegu, ustawionym na grzanie góra–dół. + ''', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'vxuL', + 'name': 'Tarta cytrynowa z bezą', + 'original_url': 'https://www.przepisy.pl/przepis/tarta-cytrynowa-z-beza', + 'recipe_id': '9d3cb303-a996-4144-948a-36afaeeef554', + 'recipe_yield': '8 servings', + 'slug': 'tarta-cytrynowa-z-beza', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Martins test Recipe', + 'original_url': None, + 'recipe_id': '77f05a49-e869-4048-aa62-0d8a1f5a8f1c', + 'recipe_yield': None, + 'slug': 'martins-test-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Muffinki czekoladowe to przepyszny i bardzo prosty w przygotowaniu mini deser pieczony w papilotkach. Przepis na najlepsze, bardzo wilgotne i puszyste muffinki czekoladowe polecam każdemu miłośnikowi czekolady.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'xP1Q', + 'name': 'Muffinki czekoladowe', + 'original_url': 'https://aniagotuje.pl/przepis/muffinki-czekoladowe', + 'recipe_id': '75a90207-9c10-4390-a265-c47a4b67fd69', + 'recipe_yield': '12', + 'slug': 'muffinki-czekoladowe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'My Test Recipe', + 'original_url': None, + 'recipe_id': '4320ba72-377b-4657-8297-dce198f24cdf', + 'recipe_yield': None, + 'slug': 'my-test-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'My Test Receipe', + 'original_url': None, + 'recipe_id': '98dac844-31ee-426a-b16c-fb62a5dd2816', + 'recipe_yield': None, + 'slug': 'my-test-receipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'r1ck', + 'name': 'Patates douces au four', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': 'c3c8f207-c704-415d-81b1-da9f032cf52f', + 'recipe_yield': '', + 'slug': 'patates-douces-au-four', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Follow these basic instructions for a thick, crisp, and chewy pizza crust at home. The recipe yields enough pizza dough for two 12-inch pizzas and you can freeze half of the dough for later. Close to 2 pounds of dough total.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'gD94', + 'name': 'Easy Homemade Pizza Dough', + 'original_url': 'https://sallysbakingaddiction.com/homemade-pizza-crust-recipe/', + 'recipe_id': '1edb2f6e-133c-4be0-b516-3c23625a97ec', + 'recipe_yield': '2 servings', + 'slug': 'easy-homemade-pizza-dough', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '356X', + 'name': 'All-American Beef Stew Recipe', + 'original_url': 'https://www.seriouseats.com/all-american-beef-stew-recipe', + 'recipe_id': '48f39d27-4b8e-4c14-bf36-4e1e6497e75e', + 'recipe_yield': '6 servings', + 'slug': 'all-american-beef-stew-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This utterly faithful recipe perfectly recreates a New York City halal-cart classic: Chicken and Rice with White Sauce. The chicken is marinated with herbs, lemon, and spices; the rice golden; the sauce, as white and creamy as ever.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '4Sys', + 'name': "Serious Eats' Halal Cart-Style Chicken and Rice With White Sauce", + 'original_url': 'https://www.seriouseats.com/serious-eats-halal-cart-style-chicken-and-rice-white-sauce-recipe', + 'recipe_id': '6530ea6e-401e-4304-8a7a-12162ddf5b9c', + 'recipe_yield': '4 servings', + 'slug': 'serious-eats-halal-cart-style-chicken-and-rice-with-white-sauce', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Schnelle Käsespätzle. Über 1201 Bewertungen und für sehr gut befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '8goY', + 'name': 'Schnelle Käsespätzle', + 'original_url': 'https://www.chefkoch.de/rezepte/1062121211526182/Schnelle-Kaesespaetzle.html', + 'recipe_id': 'c496cf9c-1ece-448a-9d3f-ef772f078a4e', + 'recipe_yield': '4 servings', + 'slug': 'schnelle-kasespatzle', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'taco', + 'original_url': None, + 'recipe_id': '49aa6f42-6760-4adf-b6cd-59592da485c3', + 'recipe_yield': None, + 'slug': 'taco', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'z8BB', + 'name': 'Vodkapasta', + 'original_url': 'https://www.ica.se/recept/vodkapasta-729011/', + 'recipe_id': '6402a253-2baa-460d-bf4f-b759bb655588', + 'recipe_yield': '4 servings', + 'slug': 'vodkapasta', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'Nqpz', + 'name': 'Vodkapasta2', + 'original_url': 'https://www.ica.se/recept/vodkapasta-729011/', + 'recipe_id': '4f54e9e1-f21d-40ec-a135-91e633dfb733', + 'recipe_yield': '4 servings', + 'slug': 'vodkapasta2', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Rub', + 'original_url': None, + 'recipe_id': 'e1a3edb0-49a0-49a3-83e3-95554e932670', + 'recipe_yield': '1', + 'slug': 'rub', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Tender and moist, these chocolate chip cookies were a HUGE hit in the Test Kitchen. They're like banana bread in a cookie form. Outside, there are crisp edges like a cookie. Inside, though, it's soft like banana bread. We opted to add chocolate chips and nuts. It's a classic flavor combination in banana bread and works just as well in these cookies.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '03XS', + 'name': 'Banana Bread Chocolate Chip Cookies', + 'original_url': 'https://www.justapinch.com/recipes/dessert/cookies/banana-bread-chocolate-chip-cookies.html', + 'recipe_id': '1a0f4e54-db5b-40f1-ab7e-166dab5f6523', + 'recipe_yield': '', + 'slug': 'banana-bread-chocolate-chip-cookies', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Hello friends, today I'm going to share with you how to make a delicious soup/bisque. A Cauliflower Bisques Recipe with Cheddar Cheese. One of my favorite soups to make when its cold outside. We will be continuing the soup collection so let me know what you think in the comments below!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'KuXV', + 'name': 'Cauliflower Bisque Recipe with Cheddar Cheese', + 'original_url': 'https://chefjeanpierre.com/recipes/soups/creamy-cauliflower-bisque/', + 'recipe_id': '447acae6-3424-4c16-8c26-c09040ad8041', + 'recipe_yield': '', + 'slug': 'cauliflower-bisque-recipe-with-cheddar-cheese', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Prova ', + 'original_url': None, + 'recipe_id': '864136a3-27b0-4f3b-a90f-486f42d6df7a', + 'recipe_yield': '', + 'slug': 'prova', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'pate au beurre (1)', + 'original_url': None, + 'recipe_id': 'c7ccf4c7-c5f4-4191-a79b-1a49d068f6a4', + 'recipe_yield': None, + 'slug': 'pate-au-beurre-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'pate au beurre', + 'original_url': None, + 'recipe_id': 'd01865c3-0f18-4e8d-84c0-c14c345fdf9c', + 'recipe_yield': None, + 'slug': 'pate-au-beurre', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Individual foolproof mason jar cheesecakes with strawberry compote and a Graham cracker crumble topping. Foolproof, simple, and delicious.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'tmwm', + 'name': 'Sous Vide Cheesecake Recipe', + 'original_url': 'https://saltpepperskillet.com/recipes/sous-vide-cheesecake/', + 'recipe_id': '2cec2bb2-19b6-40b8-a36c-1a76ea29c517', + 'recipe_yield': '4 servings', + 'slug': 'sous-vide-cheesecake-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This is a variation of the several cheese cake recipes that have been used for sous vide. These make a fabulous 4oz cheese cake for dessert. Garnish with a raspberry or blackberry and impress your family and friends. They’ll keep great in the fridge for a week easily.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'xCYc', + 'name': 'The Bomb Mini Cheesecakes', + 'original_url': 'https://recipes.anovaculinary.com/recipe/the-bomb-cheesecakes', + 'recipe_id': '8e0e4566-9caf-4c2e-a01c-dcead23db86b', + 'recipe_yield': '10 servings', + 'slug': 'the-bomb-mini-cheesecakes', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Tagliatelle al Salmone - wie beim Italiener. Über 1568 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'qzaN', + 'name': 'Tagliatelle al Salmone', + 'original_url': 'https://www.chefkoch.de/rezepte/2109501340136606/Tagliatelle-al-Salmone.html', + 'recipe_id': 'a051eafd-9712-4aee-a8e5-0cd10a6772ee', + 'recipe_yield': '4 servings', + 'slug': 'tagliatelle-al-salmone', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Hier ist der Name Programm: Den "Tod durch Schokolade" müsst ihr zwar hoffentlich nicht erleiden, aber Chocoholics werden diesen Kuchen lieben!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'K9qP', + 'name': 'Death by Chocolate', + 'original_url': 'https://www.backenmachtgluecklich.de/rezepte/death-by-chocolate-kuchen.html', + 'recipe_id': '093d51e9-0823-40ad-8e0e-a1d5790dd627', + 'recipe_yield': '1 serving', + 'slug': 'death-by-chocolate', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Palak Dal ist in Grunde genommen Spinat (Palak) mit Linsen oder anderen Hülsenfrüchten (Dal) vom indischen Subkontinent. Es kommen noch Zwiebeln, Tomaten und einige indische Gewürze dazu. Damit ist das Palak Dal ein super einfaches und zugleich veganes indisches Rezept. Es schmeckt hervorragend mit Naan-Brot und etwas gewürztem Joghurt.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'jKQ3', + 'name': 'Palak Dal Rezept aus Indien', + 'original_url': 'https://www.fernweh-koch.de/palak-dal-indischer-spinat-linsen-rezept/', + 'recipe_id': '2d1f62ec-4200-4cfd-987e-c75755d7607c', + 'recipe_yield': '4 servings', + 'slug': 'palak-dal-rezept-aus-indien', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Tortelline - á la Romana. Über 13 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'rkSn', + 'name': 'Tortelline - á la Romana', + 'original_url': 'https://www.chefkoch.de/rezepte/74441028021809/Tortelline-a-la-Romana.html', + 'recipe_id': '973dc36d-1661-49b4-ad2d-0b7191034fb3', + 'recipe_yield': '4 servings', + 'slug': 'tortelline-a-la-romana', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + ]), + }), + }) +# --- # name: test_service_import_recipe dict({ 'recipe': dict({ diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index 57c55159bdc..2ced94a7399 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -21,6 +21,8 @@ from homeassistant.components.mealie.const import ( ATTR_NOTE_TEXT, ATTR_NOTE_TITLE, ATTR_RECIPE_ID, + ATTR_RESULT_LIMIT, + ATTR_SEARCH_TERMS, ATTR_START_DATE, ATTR_URL, DOMAIN, @@ -28,6 +30,7 @@ from homeassistant.components.mealie.const import ( from homeassistant.components.mealie.services import ( SERVICE_GET_MEALPLAN, SERVICE_GET_RECIPE, + SERVICE_GET_RECIPES, SERVICE_IMPORT_RECIPE, SERVICE_SET_MEALPLAN, SERVICE_SET_RANDOM_MEALPLAN, @@ -150,6 +153,42 @@ async def test_service_recipe( assert response == snapshot +@pytest.mark.parametrize( + "service_data", + [ + # Default call + {ATTR_CONFIG_ENTRY_ID: "mock_entry_id"}, + # With search terms and result limit + { + ATTR_CONFIG_ENTRY_ID: "mock_entry_id", + ATTR_SEARCH_TERMS: "pasta", + ATTR_RESULT_LIMIT: 5, + }, + ], +) +async def test_service_get_recipes( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + service_data: dict, +) -> None: + """Test the get_recipes service.""" + await setup_integration(hass, mock_config_entry) + + # Patch entry_id into service_data for each run + service_data = {**service_data, ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_RECIPES, + service_data, + blocking=True, + return_response=True, + ) + assert response == snapshot + + async def test_service_import_recipe( hass: HomeAssistant, mock_mealie_client: AsyncMock, @@ -332,6 +371,22 @@ async def test_service_set_mealplan( ServiceValidationError, "Recipe with ID or slug `recipe_id` not found", ), + ( + SERVICE_GET_RECIPES, + {}, + "get_recipes", + MealieConnectionError, + HomeAssistantError, + "Error connecting to Mealie instance", + ), + ( + SERVICE_GET_RECIPES, + {ATTR_SEARCH_TERMS: "pasta"}, + "get_recipes", + MealieNotFoundError, + ServiceValidationError, + "No recipes found matching your search", + ), ( SERVICE_IMPORT_RECIPE, {ATTR_URL: "http://example.com"}, @@ -402,6 +457,11 @@ async def test_services_connection_error( [ (SERVICE_GET_MEALPLAN, {}), (SERVICE_GET_RECIPE, {ATTR_RECIPE_ID: "recipe_id"}), + (SERVICE_GET_RECIPES, {}), + ( + SERVICE_GET_RECIPES, + {ATTR_SEARCH_TERMS: "pasta", ATTR_RESULT_LIMIT: 5}, + ), (SERVICE_IMPORT_RECIPE, {ATTR_URL: "http://example.com"}), ( SERVICE_SET_RANDOM_MEALPLAN, From 99ee56a4ddb350389d101adb7f43267c75609734 Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 30 Jul 2025 07:45:03 -0600 Subject: [PATCH 131/265] Add Precipitation sensors to Weatherflow Cloud (#149619) Co-authored-by: Joost Lekkerkerker --- .../components/weatherflow_cloud/icons.json | 55 ++ .../components/weatherflow_cloud/sensor.py | 83 +++ .../components/weatherflow_cloud/strings.json | 28 + .../snapshots/test_sensor.ambr | 516 ++++++++++++++++++ 4 files changed, 682 insertions(+) diff --git a/homeassistant/components/weatherflow_cloud/icons.json b/homeassistant/components/weatherflow_cloud/icons.json index 5b9cd9c6cf4..a5759d8b810 100644 --- a/homeassistant/components/weatherflow_cloud/icons.json +++ b/homeassistant/components/weatherflow_cloud/icons.json @@ -34,6 +34,60 @@ "lightning_strike_last_epoch": { "default": "mdi:lightning-bolt" }, + + "precip_accum_local_day": { + "default": "mdi:umbrella-closed", + "range": { + "0.01": "mdi:umbrella" + } + }, + "precip_accum_local_day_final": { + "default": "mdi:umbrella-closed", + "range": { + "0.01": "mdi:umbrella" + } + }, + "precip_accum_local_yesterday": { + "default": "mdi:umbrella-closed", + "range": { + "0.01": "mdi:umbrella" + } + }, + "precip_accum_local_yesterday_final": { + "default": "mdi:umbrella-closed", + "range": { + "0.01": "mdi:umbrella" + } + }, + + "precip_minutes_local_day": { + "default": "mdi:umbrella-closed", + "range": { + "1": "mdi:umbrella" + } + }, + "precip_minutes_local_yesterday": { + "default": "mdi:umbrella-closed", + "range": { + "1": "mdi:umbrella" + } + }, + "precip_minutes_local_yesterday_final": { + "default": "mdi:umbrella-closed", + "range": { + "1": "mdi:umbrella" + } + }, + + "precip_analysis_type_yesterday": { + "default": "mdi:radar", + "state": { + "rain": "mdi:weather-rainy", + "snow": "mdi:weather-snowy", + "rain_snow": "mdi:weather-snoy-rainy", + "lightning": "mdi:weather-lightning-rainy" + } + }, "sea_level_pressure": { "default": "mdi:gauge" }, @@ -49,6 +103,7 @@ "wind_chill": { "default": "mdi:snowflake-thermometer" }, + "wind_direction": { "default": "mdi:compass", "range": { diff --git a/homeassistant/components/weatherflow_cloud/sensor.py b/homeassistant/components/weatherflow_cloud/sensor.py index 42357807d17..ec094448519 100644 --- a/homeassistant/components/weatherflow_cloud/sensor.py +++ b/homeassistant/components/weatherflow_cloud/sensor.py @@ -39,6 +39,14 @@ from .const import DOMAIN from .coordinator import WeatherFlowObservationCoordinator, WeatherFlowWindCoordinator from .entity import WeatherFlowCloudEntity +PRECIPITATION_TYPE = { + 0: "none", + 1: "rain", + 2: "snow", + 3: "sleet", + 4: "storm", +} + @dataclass(frozen=True, kw_only=True) class WeatherFlowCloudSensorEntityDescription( @@ -223,6 +231,81 @@ WF_SENSORS: tuple[WeatherFlowCloudSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=3, ), + # Rain Sensors + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_last_1hr", + translation_key="precip_accum_last_1hr", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_last_1hr, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_local_day", + translation_key="precip_accum_local_day", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_local_day, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_local_day_final", + translation_key="precip_accum_local_day_final", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_local_day_final, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_local_yesterday", + translation_key="precip_accum_local_yesterday", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_local_yesterday, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_local_yesterday_final", + translation_key="precip_accum_local_yesterday_final", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_local_yesterday_final, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_analysis_type_yesterday", + translation_key="precip_analysis_type_yesterday", + device_class=SensorDeviceClass.ENUM, + options=["none", "rain", "snow", "sleet", "storm"], + suggested_display_precision=1, + value_fn=lambda data: PRECIPITATION_TYPE.get( + data.precip_analysis_type_yesterday + ), + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_minutes_local_day", + translation_key="precip_minutes_local_day", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_display_precision=1, + value_fn=lambda data: data.precip_minutes_local_day, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_minutes_local_yesterday", + translation_key="precip_minutes_local_yesterday", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_display_precision=1, + value_fn=lambda data: data.precip_minutes_local_yesterday, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_minutes_local_yesterday_final", + translation_key="precip_minutes_local_yesterday_final", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_display_precision=1, + value_fn=lambda data: data.precip_minutes_local_yesterday_final, + ), # Lightning Sensors WeatherFlowCloudSensorEntityDescription( key="lightning_strike_count", diff --git a/homeassistant/components/weatherflow_cloud/strings.json b/homeassistant/components/weatherflow_cloud/strings.json index 6c6e6f122a4..5b628e9f5c8 100644 --- a/homeassistant/components/weatherflow_cloud/strings.json +++ b/homeassistant/components/weatherflow_cloud/strings.json @@ -56,6 +56,34 @@ "lightning_strike_last_epoch": { "name": "Lightning last strike" }, + "precip_accum_last_1hr": { + "name": "Rain last hour" + }, + + "precip_accum_local_day": { + "name": "Precipitation today" + }, + "precip_accum_local_day_final": { + "name": "Nearcast precipitation today" + }, + "precip_accum_local_yesterday": { + "name": "Precipitation yesterday" + }, + "precip_accum_local_yesterday_final": { + "name": "Nearcast precipitation yesterday" + }, + "precip_analysis_type_yesterday": { + "name": "Precipitation type yesterday" + }, + "precip_minutes_local_day": { + "name": "Precipitation duration today" + }, + "precip_minutes_local_yesterday": { + "name": "Precipitation duration yesterday" + }, + "precip_minutes_local_yesterday_final": { + "name": "Nearcast precipitation duration yesterday" + }, "sea_level_pressure": { "name": "Pressure sea level" }, diff --git a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr index a34d885b77b..cd6280077a2 100644 --- a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr +++ b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr @@ -489,6 +489,466 @@ 'state': '2024-02-07T23:01:15+00:00', }) # --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_duration_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.my_home_station_nearcast_precipitation_duration_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Nearcast precipitation duration yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_minutes_local_yesterday_final', + 'unique_id': '24432_precip_minutes_local_yesterday_final', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_duration_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Nearcast precipitation duration yesterday', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_duration_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.my_home_station_nearcast_precipitation_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Nearcast precipitation today', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_local_day_final', + 'unique_id': '24432_precip_accum_local_day_final', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Nearcast precipitation today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.my_home_station_nearcast_precipitation_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Nearcast precipitation yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_local_yesterday_final', + 'unique_id': '24432_precip_accum_local_yesterday_final', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Nearcast precipitation yesterday', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_duration_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.my_home_station_precipitation_duration_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation duration today', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_minutes_local_day', + 'unique_id': '24432_precip_minutes_local_day', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_duration_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Precipitation duration today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_duration_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_duration_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.my_home_station_precipitation_duration_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation duration yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_minutes_local_yesterday', + 'unique_id': '24432_precip_minutes_local_yesterday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_duration_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Precipitation duration yesterday', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_duration_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.my_home_station_precipitation_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation today', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_local_day', + 'unique_id': '24432_precip_accum_local_day', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Precipitation today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_type_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'rain', + 'snow', + 'sleet', + 'storm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_precipitation_type_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation type yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_analysis_type_yesterday', + 'unique_id': '24432_precip_analysis_type_yesterday', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_type_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'device_class': 'enum', + 'friendly_name': 'My Home Station Precipitation type yesterday', + 'options': list([ + 'none', + 'rain', + 'snow', + 'sleet', + 'storm', + ]), + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_type_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.my_home_station_precipitation_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_local_yesterday', + 'unique_id': '24432_precip_accum_local_yesterday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Precipitation yesterday', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_entities[sensor.my_home_station_pressure_barometric-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -609,6 +1069,62 @@ 'state': '1006.2', }) # --- +# name: test_all_entities[sensor.my_home_station_rain_last_hour-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.my_home_station_rain_last_hour', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Rain last hour', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_last_1hr', + 'unique_id': '24432_precip_accum_last_1hr', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_rain_last_hour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Rain last hour', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_rain_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_entities[sensor.my_home_station_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 223c34056d28bba0fd6250c7c1ea081e9a6dcb05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 30 Jul 2025 15:58:43 +0200 Subject: [PATCH 132/265] Add missing colons in miele messages (#149668) --- homeassistant/components/miele/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index cec4a63feec..01f13c8550d 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -1063,13 +1063,13 @@ "message": "Invalid device targeted." }, "get_programs_error": { - "message": "'Get programs' action failed {status} / {message}." + "message": "'Get programs' action failed: {status} / {message}" }, "set_program_error": { - "message": "'Set program' action failed {status} / {message}." + "message": "'Set program' action failed: {status} / {message}" }, "set_program_oven_error": { - "message": "'Set program on oven' action failed {status} / {message}." + "message": "'Set program on oven' action failed: {status} / {message}" }, "set_state_error": { "message": "Failed to set state for {entity}." From 1b58809655bebe1f50159efc5670f3a5459696c6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Jul 2025 16:01:44 +0200 Subject: [PATCH 133/265] Add AI Task to OpenRouter (#149275) --- .../components/open_router/__init__.py | 2 +- .../components/open_router/ai_task.py | 75 +++++++ .../components/open_router/config_flow.py | 82 ++++++- .../components/open_router/conversation.py | 2 + .../components/open_router/entity.py | 96 ++++++-- .../components/open_router/strings.json | 23 +- tests/components/open_router/conftest.py | 21 +- .../open_router/fixtures/models.json | 1 + .../open_router/snapshots/test_ai_task.ambr | 53 +++++ tests/components/open_router/test_ai_task.py | 210 ++++++++++++++++++ .../open_router/test_config_flow.py | 66 +++++- .../open_router/test_conversation.py | 9 +- 12 files changed, 601 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/open_router/ai_task.py create mode 100644 tests/components/open_router/snapshots/test_ai_task.ambr create mode 100644 tests/components/open_router/test_ai_task.py diff --git a/homeassistant/components/open_router/__init__.py b/homeassistant/components/open_router/__init__.py index 477fabca54c..9850f72f71d 100644 --- a/homeassistant/components/open_router/__init__.py +++ b/homeassistant/components/open_router/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers.httpx_client import get_async_client from .const import LOGGER -PLATFORMS = [Platform.CONVERSATION] +PLATFORMS = [Platform.AI_TASK, Platform.CONVERSATION] type OpenRouterConfigEntry = ConfigEntry[AsyncOpenAI] diff --git a/homeassistant/components/open_router/ai_task.py b/homeassistant/components/open_router/ai_task.py new file mode 100644 index 00000000000..fa5d8d0f68e --- /dev/null +++ b/homeassistant/components/open_router/ai_task.py @@ -0,0 +1,75 @@ +"""AI Task integration for OpenRouter.""" + +from __future__ import annotations + +from json import JSONDecodeError +import logging + +from homeassistant.components import ai_task, conversation +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util.json import json_loads + +from . import OpenRouterConfigEntry +from .entity import OpenRouterEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: OpenRouterConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up AI Task entities.""" + for subentry in config_entry.subentries.values(): + if subentry.subentry_type != "ai_task_data": + continue + + async_add_entities( + [OpenRouterAITaskEntity(config_entry, subentry)], + config_subentry_id=subentry.subentry_id, + ) + + +class OpenRouterAITaskEntity( + ai_task.AITaskEntity, + OpenRouterEntity, +): + """OpenRouter AI Task entity.""" + + _attr_name = None + _attr_supported_features = ai_task.AITaskEntityFeature.GENERATE_DATA + + async def _async_generate_data( + self, + task: ai_task.GenDataTask, + chat_log: conversation.ChatLog, + ) -> ai_task.GenDataTaskResult: + """Handle a generate data task.""" + await self._async_handle_chat_log(chat_log, task.name, task.structure) + + if not isinstance(chat_log.content[-1], conversation.AssistantContent): + raise HomeAssistantError( + "Last content in chat log is not an AssistantContent" + ) + + text = chat_log.content[-1].content or "" + + if not task.structure: + return ai_task.GenDataTaskResult( + conversation_id=chat_log.conversation_id, + data=text, + ) + try: + data = json_loads(text) + except JSONDecodeError as err: + raise HomeAssistantError( + "Error with OpenRouter structured response" + ) from err + + return ai_task.GenDataTaskResult( + conversation_id=chat_log.conversation_id, + data=data, + ) diff --git a/homeassistant/components/open_router/config_flow.py b/homeassistant/components/open_router/config_flow.py index 96f3769575b..2afe2129a4c 100644 --- a/homeassistant/components/open_router/config_flow.py +++ b/homeassistant/components/open_router/config_flow.py @@ -5,7 +5,12 @@ from __future__ import annotations import logging from typing import Any -from python_open_router import Model, OpenRouterClient, OpenRouterError +from python_open_router import ( + Model, + OpenRouterClient, + OpenRouterError, + SupportedParameter, +) import voluptuous as vol from homeassistant.config_entries import ( @@ -43,7 +48,10 @@ class OpenRouterConfigFlow(ConfigFlow, domain=DOMAIN): cls, config_entry: ConfigEntry ) -> dict[str, type[ConfigSubentryFlow]]: """Return subentries supported by this handler.""" - return {"conversation": ConversationFlowHandler} + return { + "conversation": ConversationFlowHandler, + "ai_task_data": AITaskDataFlowHandler, + } async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -78,13 +86,26 @@ class OpenRouterConfigFlow(ConfigFlow, domain=DOMAIN): ) -class ConversationFlowHandler(ConfigSubentryFlow): - """Handle subentry flow.""" +class OpenRouterSubentryFlowHandler(ConfigSubentryFlow): + """Handle subentry flow for OpenRouter.""" def __init__(self) -> None: """Initialize the subentry flow.""" self.models: dict[str, Model] = {} + async def _get_models(self) -> None: + """Fetch models from OpenRouter.""" + entry = self._get_entry() + client = OpenRouterClient( + entry.data[CONF_API_KEY], async_get_clientsession(self.hass) + ) + models = await client.get_models() + self.models = {model.id: model for model in models} + + +class ConversationFlowHandler(OpenRouterSubentryFlowHandler): + """Handle subentry flow.""" + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> SubentryFlowResult: @@ -95,14 +116,16 @@ class ConversationFlowHandler(ConfigSubentryFlow): return self.async_create_entry( title=self.models[user_input[CONF_MODEL]].name, data=user_input ) - entry = self._get_entry() - client = OpenRouterClient( - entry.data[CONF_API_KEY], async_get_clientsession(self.hass) - ) - models = await client.get_models() - self.models = {model.id: model for model in models} + try: + await self._get_models() + except OpenRouterError: + return self.async_abort(reason="cannot_connect") + except Exception: + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") options = [ - SelectOptionDict(value=model.id, label=model.name) for model in models + SelectOptionDict(value=model.id, label=model.name) + for model in self.models.values() ] hass_apis: list[SelectOptionDict] = [ @@ -138,3 +161,40 @@ class ConversationFlowHandler(ConfigSubentryFlow): } ), ) + + +class AITaskDataFlowHandler(OpenRouterSubentryFlowHandler): + """Handle subentry flow.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> SubentryFlowResult: + """User flow to create a sensor subentry.""" + if user_input is not None: + return self.async_create_entry( + title=self.models[user_input[CONF_MODEL]].name, data=user_input + ) + try: + await self._get_models() + except OpenRouterError: + return self.async_abort(reason="cannot_connect") + except Exception: + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + options = [ + SelectOptionDict(value=model.id, label=model.name) + for model in self.models.values() + if SupportedParameter.STRUCTURED_OUTPUTS in model.supported_parameters + ] + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_MODEL): SelectSelector( + SelectSelectorConfig( + options=options, mode=SelectSelectorMode.DROPDOWN, sort=True + ), + ), + } + ), + ) diff --git a/homeassistant/components/open_router/conversation.py b/homeassistant/components/open_router/conversation.py index 826931d3da7..3c185ecd77c 100644 --- a/homeassistant/components/open_router/conversation.py +++ b/homeassistant/components/open_router/conversation.py @@ -20,6 +20,8 @@ async def async_setup_entry( ) -> None: """Set up conversation entities.""" for subentry_id, subentry in config_entry.subentries.items(): + if subentry.subentry_type != "conversation": + continue async_add_entities( [OpenRouterConversationEntity(config_entry, subentry)], config_subentry_id=subentry_id, diff --git a/homeassistant/components/open_router/entity.py b/homeassistant/components/open_router/entity.py index e706656d377..ac01ec89704 100644 --- a/homeassistant/components/open_router/entity.py +++ b/homeassistant/components/open_router/entity.py @@ -4,10 +4,9 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Callable import json -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import openai -from openai import NOT_GIVEN from openai.types.chat import ( ChatCompletionAssistantMessageParam, ChatCompletionMessage, @@ -19,7 +18,9 @@ from openai.types.chat import ( ChatCompletionUserMessageParam, ) from openai.types.chat.chat_completion_message_tool_call_param import Function -from openai.types.shared_params import FunctionDefinition +from openai.types.shared_params import FunctionDefinition, ResponseFormatJSONSchema +from openai.types.shared_params.response_format_json_schema import JSONSchema +import voluptuous as vol from voluptuous_openapi import convert from homeassistant.components import conversation @@ -36,6 +37,50 @@ from .const import DOMAIN, LOGGER MAX_TOOL_ITERATIONS = 10 +def _adjust_schema(schema: dict[str, Any]) -> None: + """Adjust the schema to be compatible with OpenRouter API.""" + if schema["type"] == "object": + if "properties" not in schema: + return + + if "required" not in schema: + schema["required"] = [] + + # Ensure all properties are required + for prop, prop_info in schema["properties"].items(): + _adjust_schema(prop_info) + if prop not in schema["required"]: + prop_info["type"] = [prop_info["type"], "null"] + schema["required"].append(prop) + + elif schema["type"] == "array": + if "items" not in schema: + return + + _adjust_schema(schema["items"]) + + +def _format_structured_output( + name: str, schema: vol.Schema, llm_api: llm.APIInstance | None +) -> JSONSchema: + """Format the schema to be compatible with OpenRouter API.""" + result: JSONSchema = { + "name": name, + "strict": True, + } + result_schema = convert( + schema, + custom_serializer=( + llm_api.custom_serializer if llm_api else llm.selector_serializer + ), + ) + + _adjust_schema(result_schema) + + result["schema"] = result_schema + return result + + def _format_tool( tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None, @@ -136,9 +181,24 @@ class OpenRouterEntity(Entity): entry_type=dr.DeviceEntryType.SERVICE, ) - async def _async_handle_chat_log(self, chat_log: conversation.ChatLog) -> None: + async def _async_handle_chat_log( + self, + chat_log: conversation.ChatLog, + structure_name: str | None = None, + structure: vol.Schema | None = None, + ) -> None: """Generate an answer for the chat log.""" + model_args = { + "model": self.model, + "user": chat_log.conversation_id, + "extra_headers": { + "X-Title": "Home Assistant", + "HTTP-Referer": "https://www.home-assistant.io/integrations/open_router", + }, + "extra_body": {"require_parameters": True}, + } + tools: list[ChatCompletionToolParam] | None = None if chat_log.llm_api: tools = [ @@ -146,33 +206,37 @@ class OpenRouterEntity(Entity): for tool in chat_log.llm_api.tools ] - messages = [ + if tools: + model_args["tools"] = tools + + model_args["messages"] = [ m for content in chat_log.content if (m := _convert_content_to_chat_message(content)) ] + if structure: + if TYPE_CHECKING: + assert structure_name is not None + model_args["response_format"] = ResponseFormatJSONSchema( + type="json_schema", + json_schema=_format_structured_output( + structure_name, structure, chat_log.llm_api + ), + ) + client = self.entry.runtime_data for _iteration in range(MAX_TOOL_ITERATIONS): try: - result = await client.chat.completions.create( - model=self.model, - messages=messages, - tools=tools or NOT_GIVEN, - user=chat_log.conversation_id, - extra_headers={ - "X-Title": "Home Assistant", - "HTTP-Referer": "https://www.home-assistant.io/integrations/open_router", - }, - ) + result = await client.chat.completions.create(**model_args) except openai.OpenAIError as err: LOGGER.error("Error talking to API: %s", err) raise HomeAssistantError("Error talking to API") from err result_message = result.choices[0].message - messages.extend( + model_args["messages"].extend( [ msg async for content in chat_log.async_add_delta_content_stream( diff --git a/homeassistant/components/open_router/strings.json b/homeassistant/components/open_router/strings.json index 91c4cc350ae..e73a65cd178 100644 --- a/homeassistant/components/open_router/strings.json +++ b/homeassistant/components/open_router/strings.json @@ -37,7 +37,28 @@ "initiate_flow": { "user": "Add conversation agent" }, - "entry_type": "Conversation agent" + "entry_type": "Conversation agent", + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "ai_task_data": { + "step": { + "user": { + "data": { + "model": "[%key:component::open_router::config_subentries::conversation::step::user::data::model%]" + } + } + }, + "initiate_flow": { + "user": "Add Generate data with AI service" + }, + "entry_type": "Generate data with AI service", + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } } } } diff --git a/tests/components/open_router/conftest.py b/tests/components/open_router/conftest.py index 7bb967f369f..33ca4d790c9 100644 --- a/tests/components/open_router/conftest.py +++ b/tests/components/open_router/conftest.py @@ -49,9 +49,19 @@ def conversation_subentry_data(enable_assist: bool) -> dict[str, Any]: return res +@pytest.fixture +def ai_task_data_subentry_data() -> dict[str, Any]: + """Mock AI task subentry data.""" + return { + CONF_MODEL: "google/gemini-1.5-pro", + } + + @pytest.fixture def mock_config_entry( - hass: HomeAssistant, conversation_subentry_data: dict[str, Any] + hass: HomeAssistant, + conversation_subentry_data: dict[str, Any], + ai_task_data_subentry_data: dict[str, Any], ) -> MockConfigEntry: """Mock a config entry.""" return MockConfigEntry( @@ -67,7 +77,14 @@ def mock_config_entry( subentry_type="conversation", title="GPT-3.5 Turbo", unique_id=None, - ) + ), + ConfigSubentryData( + data=ai_task_data_subentry_data, + subentry_id="ABCDEG", + subentry_type="ai_task_data", + title="Gemini 1.5 Pro", + unique_id=None, + ), ], ) diff --git a/tests/components/open_router/fixtures/models.json b/tests/components/open_router/fixtures/models.json index 0a35686094e..b17f584c0e6 100644 --- a/tests/components/open_router/fixtures/models.json +++ b/tests/components/open_router/fixtures/models.json @@ -85,6 +85,7 @@ "logit_bias", "logprobs", "top_logprobs", + "structured_outputs", "response_format" ] } diff --git a/tests/components/open_router/snapshots/test_ai_task.ambr b/tests/components/open_router/snapshots/test_ai_task.ambr new file mode 100644 index 00000000000..0839f6fef9b --- /dev/null +++ b/tests/components/open_router/snapshots/test_ai_task.ambr @@ -0,0 +1,53 @@ +# serializer version: 1 +# name: test_all_entities[ai_task.gemini_1_5_pro-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'ai_task', + 'entity_category': None, + 'entity_id': 'ai_task.gemini_1_5_pro', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'conversation': dict({ + 'should_expose': False, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'open_router', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'ABCDEG', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[ai_task.gemini_1_5_pro-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Gemini 1.5 Pro', + 'supported_features': , + }), + 'context': , + 'entity_id': 'ai_task.gemini_1_5_pro', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/open_router/test_ai_task.py b/tests/components/open_router/test_ai_task.py new file mode 100644 index 00000000000..0b6c2933be7 --- /dev/null +++ b/tests/components/open_router/test_ai_task.py @@ -0,0 +1,210 @@ +"""Test AI Task structured data generation.""" + +from unittest.mock import AsyncMock, patch + +from openai.types import CompletionUsage +from openai.types.chat import ChatCompletion, ChatCompletionMessage +from openai.types.chat.chat_completion import Choice +import pytest +from syrupy.assertion import SnapshotAssertion +import voluptuous as vol + +from homeassistant.components import ai_task +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er, selector + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_openai_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.open_router.PLATFORMS", + [Platform.AI_TASK], + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_generate_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openai_client: AsyncMock, +) -> None: + """Test AI Task data generation.""" + await setup_integration(hass, mock_config_entry) + + entity_id = "ai_task.gemini_1_5_pro" + + mock_openai_client.chat.completions.create = AsyncMock( + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="The test data", + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="x-ai/grok-3", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ) + ) + + result = await ai_task.async_generate_data( + hass, + task_name="Test Task", + entity_id=entity_id, + instructions="Generate test data", + ) + + assert result.data == "The test data" + + +async def test_generate_structured_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openai_client: AsyncMock, +) -> None: + """Test AI Task structured data generation.""" + await setup_integration(hass, mock_config_entry) + + mock_openai_client.chat.completions.create = AsyncMock( + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content='{"characters": ["Mario", "Luigi"]}', + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="x-ai/grok-3", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ) + ) + + result = await ai_task.async_generate_data( + hass, + task_name="Test Task", + entity_id="ai_task.gemini_1_5_pro", + instructions="Generate test data", + structure=vol.Schema( + { + vol.Required("characters"): selector.selector( + { + "text": { + "multiple": True, + } + } + ) + }, + ), + ) + + assert result.data == {"characters": ["Mario", "Luigi"]} + assert mock_openai_client.chat.completions.create.call_args_list[0][1][ + "response_format" + ] == { + "json_schema": { + "name": "Test Task", + "schema": { + "properties": { + "characters": { + "items": {"type": "string"}, + "type": "array", + } + }, + "required": ["characters"], + "type": "object", + }, + "strict": True, + }, + "type": "json_schema", + } + + +async def test_generate_invalid_structured_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openai_client: AsyncMock, +) -> None: + """Test AI Task with invalid JSON response.""" + await setup_integration(hass, mock_config_entry) + + mock_openai_client.chat.completions.create = AsyncMock( + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="INVALID JSON RESPONSE", + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="x-ai/grok-3", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ) + ) + + with pytest.raises( + HomeAssistantError, match="Error with OpenRouter structured response" + ): + await ai_task.async_generate_data( + hass, + task_name="Test Task", + entity_id="ai_task.gemini_1_5_pro", + instructions="Generate test data", + structure=vol.Schema( + { + vol.Required("characters"): selector.selector( + { + "text": { + "multiple": True, + } + } + ) + }, + ), + ) diff --git a/tests/components/open_router/test_config_flow.py b/tests/components/open_router/test_config_flow.py index 0720f6d90f5..b406e75507b 100644 --- a/tests/components/open_router/test_config_flow.py +++ b/tests/components/open_router/test_config_flow.py @@ -110,9 +110,6 @@ async def test_create_conversation_agent( mock_config_entry: MockConfigEntry, ) -> None: """Test creating a conversation agent.""" - - mock_config_entry.add_to_hass(hass) - await setup_integration(hass, mock_config_entry) result = await hass.config_entries.subentries.async_init( @@ -152,9 +149,6 @@ async def test_create_conversation_agent_no_control( mock_config_entry: MockConfigEntry, ) -> None: """Test creating a conversation agent without control over the LLM API.""" - - mock_config_entry.add_to_hass(hass) - await setup_integration(hass, mock_config_entry) result = await hass.config_entries.subentries.async_init( @@ -184,3 +178,63 @@ async def test_create_conversation_agent_no_control( CONF_MODEL: "openai/gpt-3.5-turbo", CONF_PROMPT: "you are an assistant", } + + +async def test_create_ai_task( + hass: HomeAssistant, + mock_open_router_client: AsyncMock, + mock_openai_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test creating an AI Task.""" + await setup_integration(hass, mock_config_entry) + + result = await hass.config_entries.subentries.async_init( + (mock_config_entry.entry_id, "ai_task_data"), + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + assert result["step_id"] == "user" + + assert result["data_schema"].schema["model"].config["options"] == [ + {"value": "openai/gpt-4", "label": "OpenAI: GPT-4"}, + ] + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + {CONF_MODEL: "openai/gpt-4"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == {CONF_MODEL: "openai/gpt-4"} + + +@pytest.mark.parametrize( + "subentry_type", + ["conversation", "ai_task_data"], +) +@pytest.mark.parametrize( + ("exception", "reason"), + [(OpenRouterError("exception"), "cannot_connect"), (Exception, "unknown")], +) +async def test_subentry_exceptions( + hass: HomeAssistant, + mock_open_router_client: AsyncMock, + mock_openai_client: AsyncMock, + mock_config_entry: MockConfigEntry, + subentry_type: str, + exception: Exception, + reason: str, +) -> None: + """Test subentry flow exceptions.""" + await setup_integration(hass, mock_config_entry) + + mock_open_router_client.get_models.side_effect = exception + + result = await hass.config_entries.subentries.async_init( + (mock_config_entry.entry_id, subentry_type), + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason diff --git a/tests/components/open_router/test_conversation.py b/tests/components/open_router/test_conversation.py index 93f8264801a..afbdd907f93 100644 --- a/tests/components/open_router/test_conversation.py +++ b/tests/components/open_router/test_conversation.py @@ -1,6 +1,6 @@ """Tests for the OpenRouter integration.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from freezegun import freeze_time from openai.types import CompletionUsage @@ -15,6 +15,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components import conversation +from homeassistant.const import Platform from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry as er, intent @@ -40,7 +41,11 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - await setup_integration(hass, mock_config_entry) + with patch( + "homeassistant.components.open_router.PLATFORMS", + [Platform.CONVERSATION], + ): + await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From fc900a632aac01c1e4e5ed6705de7c99b91c2cdc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:04:45 +0200 Subject: [PATCH 134/265] Revert logging for unsupported Tuya devices (#149665) --- homeassistant/components/tuya/__init__.py | 11 ----------- tests/components/tuya/test_init.py | 9 --------- 2 files changed, 20 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 6c3aa146158..106075e9314 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -153,17 +153,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool # Register known device IDs device_registry = dr.async_get(hass) for device in manager.device_map.values(): - if not device.status and not device.status_range and not device.function: - # If the device has no status, status_range or function, - # it cannot be supported - LOGGER.info( - "Device %s (%s) has been ignored as it does not provide any" - " standard instructions (status, status_range and function are" - " all empty) - see %s", - device.product_name, - device.id, - "https://github.com/tuya/tuya-device-sharing-sdk/issues/11", - ) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, device.id)}, diff --git a/tests/components/tuya/test_init.py b/tests/components/tuya/test_init.py index 8fbf6fb4e3b..9e9855f9fac 100644 --- a/tests/components/tuya/test_init.py +++ b/tests/components/tuya/test_init.py @@ -24,7 +24,6 @@ async def test_unsupported_device( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, - caplog: pytest.LogCaptureFixture, ) -> None: """Test unsupported device.""" @@ -39,11 +38,3 @@ async def test_unsupported_device( assert not er.async_entries_for_config_entry( entity_registry, mock_config_entry.entry_id ) - - # Information log entry added - assert ( - "Device DOLCECLIMA 10 HP WIFI (mock_device_id) has been ignored" - " as it does not provide any standard instructions (status, status_range" - " and function are all empty) - see " - "https://github.com/tuya/tuya-device-sharing-sdk/issues/11" in caplog.text - ) From 160b61e0b9c57a25def6153058996c342e752a3e Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:17:49 -0400 Subject: [PATCH 135/265] Add config flow to template fan platform (#149446) Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> --- .../components/template/config_flow.py | 33 ++++++++++ homeassistant/components/template/fan.py | 46 +++++++++++++- .../components/template/strings.json | 60 ++++++++++++++++++ .../template/snapshots/test_fan.ambr | 15 +++++ tests/components/template/test_config_flow.py | 32 ++++++++++ tests/components/template/test_fan.py | 61 ++++++++++++++++++- 6 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 tests/components/template/snapshots/test_fan.ambr diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index c9028d058bf..8653a2f4646 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -72,6 +72,14 @@ from .cover import ( STOP_ACTION, async_create_preview_cover, ) +from .fan import ( + CONF_OFF_ACTION, + CONF_ON_ACTION, + CONF_PERCENTAGE, + CONF_SET_PERCENTAGE_ACTION, + CONF_SPEED_COUNT, + async_create_preview_fan, +) from .light import ( CONF_HS, CONF_HS_ACTION, @@ -182,6 +190,19 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: ) } + if domain == Platform.FAN: + schema |= _SCHEMA_STATE | { + vol.Required(CONF_ON_ACTION): selector.ActionSelector(), + vol.Required(CONF_OFF_ACTION): selector.ActionSelector(), + vol.Optional(CONF_PERCENTAGE): selector.TemplateSelector(), + vol.Optional(CONF_SET_PERCENTAGE_ACTION): selector.ActionSelector(), + vol.Optional(CONF_SPEED_COUNT): selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=100, step=1, mode=selector.NumberSelectorMode.BOX + ), + ), + } + if domain == Platform.IMAGE: schema |= { vol.Required(CONF_URL): selector.TemplateSelector(), @@ -379,6 +400,7 @@ TEMPLATE_TYPES = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.COVER, + Platform.FAN, Platform.IMAGE, Platform.LIGHT, Platform.NUMBER, @@ -408,6 +430,11 @@ CONFIG_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.COVER), ), + Platform.FAN: SchemaFlowFormStep( + config_schema(Platform.FAN), + preview="template", + validate_user_input=validate_user_input(Platform.FAN), + ), Platform.IMAGE: SchemaFlowFormStep( config_schema(Platform.IMAGE), preview="template", @@ -462,6 +489,11 @@ OPTIONS_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.COVER), ), + Platform.FAN: SchemaFlowFormStep( + options_schema(Platform.FAN), + preview="template", + validate_user_input=validate_user_input(Platform.FAN), + ), Platform.IMAGE: SchemaFlowFormStep( options_schema(Platform.IMAGE), preview="template", @@ -501,6 +533,7 @@ CREATE_PREVIEW_ENTITY: dict[ Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel, Platform.BINARY_SENSOR: async_create_preview_binary_sensor, Platform.COVER: async_create_preview_cover, + Platform.FAN: async_create_preview_fan, Platform.LIGHT: async_create_preview_light, Platform.NUMBER: async_create_preview_number, Platform.SELECT: async_create_preview_select, diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 381d58a8a9c..9504ba45ab9 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -20,6 +20,7 @@ from homeassistant.components.fan import ( FanEntity, FanEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, @@ -34,15 +35,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .coordinator import TriggerUpdateCoordinator from .entity import AbstractTemplateEntity -from .helpers import async_setup_template_platform +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) from .template_entity import ( TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, @@ -132,6 +141,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_LEGACY_YAML_SCHEMA)} ) +FAN_CONFIG_ENTRY_SCHEMA = FAN_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + async def async_setup_platform( hass: HomeAssistant, @@ -153,6 +166,35 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + StateFanEntity, + FAN_CONFIG_ENTRY_SCHEMA, + ) + + +@callback +def async_create_preview_fan( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateFanEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateFanEntity, + FAN_CONFIG_ENTRY_SCHEMA, + ) + + class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): """Representation of a template fan features.""" diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 070dd75865f..f1c754a1e61 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -111,6 +111,36 @@ }, "title": "Template cover" }, + "fan": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "percentage": "Percentage", + "set_percentage": "Actions on set percentage", + "speed_count": "Speed count" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the fan. Valid values: `on`, `off`.", + "turn_off": "Defines actions to run when the fan is turned off.", + "turn_on": "Defines actions to run when the fan is turned on.", + "percentage": "Defines a template to get the speed percentage of the fan.", + "set_percentage": "Defines actions to run when the fan is given a speed percentage command.", + "speed_count": "The number of speeds the fan supports. Used to calculate the percentage step for the `fan.increase_speed` and `fan.decrease_speed` actions." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "Template fan" + }, "image": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -232,6 +262,7 @@ "binary_sensor": "Template a binary sensor", "button": "Template a button", "cover": "Template a cover", + "fan": "Template a fan", "image": "Template an image", "light": "Template a light", "number": "Template a number", @@ -360,6 +391,35 @@ }, "title": "[%key:component::template::config::step::cover::title%]" }, + "fan": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "percentage": "[%key:component::template::config::step::fan::data::percentage%]", + "set_percentage": "[%key:component::template::config::step::fan::data::set_percentage%]", + "speed_count": "[%key:component::template::config::step::fan::data::speed_count%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::fan::data_description::state%]", + "turn_off": "[%key:component::template::config::step::fan::data_description::turn_off%]", + "turn_on": "[%key:component::template::config::step::fan::data_description::turn_on%]", + "percentage": "[%key:component::template::config::step::fan::data_description::percentage%]", + "set_percentage": "[%key:component::template::config::step::fan::data_description::set_percentage%]", + "speed_count": "[%key:component::template::config::step::fan::data_description::speed_count%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "[%key:component::template::config::step::fan::title%]" + }, "image": { "data": { "device_id": "[%key:common::config_flow::data::device%]", diff --git a/tests/components/template/snapshots/test_fan.ambr b/tests/components/template/snapshots/test_fan.ambr new file mode 100644 index 00000000000..3026176ef97 --- /dev/null +++ b/tests/components/template/snapshots/test_fan.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index ad992eec79d..68d78ab7a27 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -149,6 +149,16 @@ BINARY_SENSOR_OPTIONS = { }, {}, ), + ( + "fan", + {"state": "{{ states('fan.one') }}"}, + "on", + {"one": "on", "two": "off"}, + {}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + {}, + ), ( "image", {"url": "{{ states('sensor.one') }}"}, @@ -332,6 +342,12 @@ async def test_config_flow( {"set_cover_position": []}, {"set_cover_position": []}, ), + ( + "fan", + {"state": "{{ states('fan.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), ( "image", { @@ -534,6 +550,16 @@ async def test_config_flow_device( {"set_cover_position": []}, "state", ), + ( + "fan", + {"state": "{{ states('fan.one') }}"}, + {"state": "{{ states('fan.two') }}"}, + ["on", "off"], + {"one": "on", "two": "off"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + "state", + ), ( "image", { @@ -1391,6 +1417,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {"set_cover_position": []}, {"set_cover_position": []}, ), + ( + "fan", + {"state": "{{ states('fan.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), ( "image", { diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index c0af18166df..b9161edf61a 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant.components import fan, template @@ -21,10 +22,11 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import ConfigurationStyle +from .conftest import ConfigurationStyle, async_get_flow_preview_state -from tests.common import assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component from tests.components.fan import common +from tests.typing import WebSocketGenerator TEST_OBJECT_ID = "test_fan" TEST_ENTITY_ID = f"fan.{TEST_OBJECT_ID}" @@ -1881,3 +1883,58 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a fan from a config entry.""" + + hass.states.async_set( + "sensor.test_sensor", + "on", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('sensor.test_sensor') }}", + "turn_on": [], + "turn_off": [], + "template_type": fan.DOMAIN, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("fan.my_template") + assert state is not None + assert state == snapshot + + +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + fan.DOMAIN, + { + "name": "My template", + "state": "{{ 'on' }}", + "turn_on": [], + "turn_off": [], + }, + ) + + assert state["state"] == STATE_ON From daea76c2f1e45705bc3a6992240917501b0751ff Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Jul 2025 16:51:10 +0200 Subject: [PATCH 136/265] Update frontend to 20250730.0 (#149672) --- 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 791acf8a39c..09461a3543a 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.3"] + "requirements": ["home-assistant-frontend==20250730.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 24c107e5611..819bb2f5c9a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==4.0.1 hass-nabucasa==0.110.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250702.3 +home-assistant-frontend==20250730.0 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 8c68449f7d4..f731ecc0e0d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250702.3 +home-assistant-frontend==20250730.0 # homeassistant.components.conversation home-assistant-intents==2025.6.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f2cf2c491e..64931e1ef4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250702.3 +home-assistant-frontend==20250730.0 # homeassistant.components.conversation home-assistant-intents==2025.6.23 From edca3fc0b714669d95d96e76992c887ed6ed892d Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 30 Jul 2025 16:52:20 +0200 Subject: [PATCH 137/265] Add matter to Third Reality (#149659) --- homeassistant/brands/third_reality.json | 2 +- homeassistant/generated/integrations.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/brands/third_reality.json b/homeassistant/brands/third_reality.json index 172b74c42fc..7a4304dad9f 100644 --- a/homeassistant/brands/third_reality.json +++ b/homeassistant/brands/third_reality.json @@ -1,5 +1,5 @@ { "domain": "third_reality", "name": "Third Reality", - "iot_standards": ["zigbee"] + "iot_standards": ["matter", "zigbee"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1eb37ae87d2..c606d79f2c5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6728,6 +6728,7 @@ "third_reality": { "name": "Third Reality", "iot_standards": [ + "matter", "zigbee" ] }, From d481a694f11e679f2f5fc78379919dec5c2d23eb Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:04:08 -0400 Subject: [PATCH 138/265] Add config flow to template vacuum platform (#149458) --- .../components/template/config_flow.py | 44 ++++++++++++++ .../components/template/strings.json | 59 ++++++++++++++++++- homeassistant/components/template/vacuum.py | 46 ++++++++++++++- .../template/snapshots/test_vacuum.ambr | 15 +++++ tests/components/template/test_config_flow.py | 32 ++++++++++ tests/components/template/test_vacuum.py | 59 ++++++++++++++++++- 6 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 tests/components/template/snapshots/test_vacuum.ambr diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 8653a2f4646..394af688152 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -103,6 +103,18 @@ from .select import CONF_OPTIONS, CONF_SELECT_OPTION, async_create_preview_selec from .sensor import async_create_preview_sensor from .switch import async_create_preview_switch from .template_entity import TemplateEntity +from .vacuum import ( + CONF_FAN_SPEED, + CONF_FAN_SPEED_LIST, + SERVICE_CLEAN_SPOT, + SERVICE_LOCATE, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + async_create_preview_vacuum, +) _SCHEMA_STATE: dict[vol.Marker, Any] = { vol.Required(CONF_STATE): selector.TemplateSelector(), @@ -294,6 +306,26 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: vol.Optional(CONF_TURN_OFF): selector.ActionSelector(), } + if domain == Platform.VACUUM: + schema |= _SCHEMA_STATE | { + vol.Required(SERVICE_START): selector.ActionSelector(), + vol.Optional(CONF_FAN_SPEED): selector.TemplateSelector(), + vol.Optional(CONF_FAN_SPEED_LIST): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[], + multiple=True, + custom_value=True, + mode=selector.SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(SERVICE_SET_FAN_SPEED): selector.ActionSelector(), + vol.Optional(SERVICE_STOP): selector.ActionSelector(), + vol.Optional(SERVICE_PAUSE): selector.ActionSelector(), + vol.Optional(SERVICE_RETURN_TO_BASE): selector.ActionSelector(), + vol.Optional(SERVICE_CLEAN_SPOT): selector.ActionSelector(), + vol.Optional(SERVICE_LOCATE): selector.ActionSelector(), + } + schema |= { vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), vol.Optional(CONF_ADVANCED_OPTIONS): section( @@ -407,6 +439,7 @@ TEMPLATE_TYPES = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.VACUUM, ] CONFIG_FLOW = { @@ -465,6 +498,11 @@ CONFIG_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.SWITCH), ), + Platform.VACUUM: SchemaFlowFormStep( + config_schema(Platform.VACUUM), + preview="template", + validate_user_input=validate_user_input(Platform.VACUUM), + ), } @@ -524,6 +562,11 @@ OPTIONS_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.SWITCH), ), + Platform.VACUUM: SchemaFlowFormStep( + options_schema(Platform.VACUUM), + preview="template", + validate_user_input=validate_user_input(Platform.VACUUM), + ), } CREATE_PREVIEW_ENTITY: dict[ @@ -539,6 +582,7 @@ CREATE_PREVIEW_ENTITY: dict[ Platform.SELECT: async_create_preview_select, Platform.SENSOR: async_create_preview_sensor, Platform.SWITCH: async_create_preview_switch, + Platform.VACUUM: async_create_preview_vacuum, } diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index f1c754a1e61..cb1e26fac78 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -268,7 +268,8 @@ "number": "Template a number", "select": "Template a select", "sensor": "Template a sensor", - "switch": "Template a switch" + "switch": "Template a switch", + "vacuum": "Template a vacuum" }, "title": "Template helper" }, @@ -293,6 +294,34 @@ } }, "title": "Template switch" + }, + "vacuum": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "start": "Actions on turn off", + "fan_speed": "Fan speed", + "fan_speeds": "Fan speeds", + "set_fan_speed": "Actions on set fan speed", + "stop": "Actions on stop", + "pause": "Actions on pause", + "return_to_base": "Actions on return to base", + "clean_spot": "Actions on clean spot", + "locate": "Actions on locate" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "Template vacuum" } } }, @@ -552,6 +581,34 @@ } }, "title": "[%key:component::template::config::step::switch::title%]" + }, + "vacuum": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "start": "[%key:component::template::config::step::vacuum::data::start%]", + "fan_speed": "[%key:component::template::config::step::vacuum::data::fan_speed%]", + "fan_speeds": "[%key:component::template::config::step::vacuum::data::fan_speeds%]", + "set_fan_speed": "[%key:component::template::config::step::vacuum::data::set_fan_speed%]", + "stop": "[%key:component::template::config::step::vacuum::data::stop%]", + "pause": "[%key:component::template::config::step::vacuum::data::pause%]", + "return_to_base": "[%key:component::template::config::step::vacuum::data::return_to_base%]", + "clean_spot": "[%key:component::template::config::step::vacuum::data::clean_spot%]", + "locate": "[%key:component::template::config::step::vacuum::data::locate%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "Template vacuum" } } }, diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 67f0f780388..1abfdbd00da 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -22,6 +22,7 @@ from homeassistant.components.vacuum import ( VacuumActivity, VacuumEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, @@ -34,16 +35,24 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .coordinator import TriggerUpdateCoordinator from .entity import AbstractTemplateEntity -from .helpers import async_setup_template_platform +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) from .template_entity import ( TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_attributes_schema, @@ -125,6 +134,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_VACUUMS): cv.schema_with_slug_keys(VACUUM_LEGACY_YAML_SCHEMA)} ) +VACUUM_CONFIG_ENTRY_SCHEMA = VACUUM_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + async def async_setup_platform( hass: HomeAssistant, @@ -146,6 +159,35 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + TemplateStateVacuumEntity, + VACUUM_CONFIG_ENTRY_SCHEMA, + ) + + +@callback +def async_create_preview_vacuum( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> TemplateStateVacuumEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + TemplateStateVacuumEntity, + VACUUM_CONFIG_ENTRY_SCHEMA, + ) + + class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity): """Representation of a template vacuum features.""" diff --git a/tests/components/template/snapshots/test_vacuum.ambr b/tests/components/template/snapshots/test_vacuum.ambr new file mode 100644 index 00000000000..01cc9c8ba82 --- /dev/null +++ b/tests/components/template/snapshots/test_vacuum.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'vacuum.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'docked', + }) +# --- diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 68d78ab7a27..9bfb0d439f7 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -229,6 +229,16 @@ BINARY_SENSOR_OPTIONS = { {}, {}, ), + ( + "vacuum", + {"state": "{{ states('vacuum.one') }}"}, + "docked", + {"one": "docked", "two": "cleaning"}, + {}, + {"start": []}, + {"start": []}, + {}, + ), ], ) @pytest.mark.freeze_time("2024-07-09 00:00:00+00:00") @@ -398,6 +408,12 @@ async def test_config_flow( {"options": "{{ ['off', 'on', 'auto'] }}"}, {"options": "{{ ['off', 'on', 'auto'] }}"}, ), + ( + "vacuum", + {"state": "{{ states('vacuum.one') }}"}, + {"start": []}, + {"start": []}, + ), ], ) async def test_config_flow_device( @@ -647,6 +663,16 @@ async def test_config_flow_device( {}, "value_template", ), + ( + "vacuum", + {"state": "{{ states('vacuum.one') }}"}, + {"state": "{{ states('vacuum.two') }}"}, + ["docked", "cleaning"], + {"one": "docked", "two": "cleaning"}, + {"start": []}, + {"start": []}, + "state", + ), ], ) @pytest.mark.freeze_time("2024-07-09 00:00:00+00:00") @@ -1480,6 +1506,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "vacuum", + {"state": "{{ states('vacuum.one') }}"}, + {"start": []}, + {"start": []}, + ), ], ) async def test_options_flow_change_device( diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 540b4eccd3b..6c7222645b6 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import template, vacuum from homeassistant.components.vacuum import ( @@ -18,10 +19,11 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -from .conftest import ConfigurationStyle +from .conftest import ConfigurationStyle, async_get_flow_preview_state -from tests.common import assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component from tests.components.vacuum import common +from tests.typing import WebSocketGenerator TEST_OBJECT_ID = "test_vacuum" TEST_ENTITY_ID = f"vacuum.{TEST_OBJECT_ID}" @@ -1261,3 +1263,56 @@ async def test_optimistic_option( state = hass.states.get(TEST_ENTITY_ID) assert state.state == VacuumActivity.DOCKED + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a vacuum from a config entry.""" + + hass.states.async_set( + "sensor.test_sensor", + "docked", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('sensor.test_sensor') }}", + "start": [], + "template_type": vacuum.DOMAIN, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("vacuum.my_template") + assert state is not None + assert state == snapshot + + +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + vacuum.DOMAIN, + { + "name": "My template", + "state": "{{ 'cleaning' }}", + "start": [], + }, + ) + + assert state["state"] == VacuumActivity.CLEANING From 6306baa3c985e7a6c47304e11a5291c0ecde460a Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:04:39 -0400 Subject: [PATCH 139/265] Add config flow to template lock platform (#149449) --- .../components/template/config_flow.py | 21 +++++++ homeassistant/components/template/lock.py | 46 +++++++++++++- .../components/template/strings.json | 46 ++++++++++++++ .../template/snapshots/test_lock.ambr | 15 +++++ tests/components/template/test_config_flow.py | 32 ++++++++++ tests/components/template/test_lock.py | 61 ++++++++++++++++++- 6 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 tests/components/template/snapshots/test_lock.ambr diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 394af688152..2e581628da2 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -89,6 +89,7 @@ from .light import ( CONF_TEMPERATURE_ACTION, async_create_preview_light, ) +from .lock import CONF_LOCK, CONF_OPEN, CONF_UNLOCK, async_create_preview_lock from .number import ( CONF_MAX, CONF_MIN, @@ -233,6 +234,14 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: vol.Optional(CONF_TEMPERATURE_ACTION): selector.ActionSelector(), } + if domain == Platform.LOCK: + schema |= _SCHEMA_STATE | { + vol.Required(CONF_LOCK): selector.ActionSelector(), + vol.Required(CONF_UNLOCK): selector.ActionSelector(), + vol.Optional(CONF_CODE_FORMAT): selector.TemplateSelector(), + vol.Optional(CONF_OPEN): selector.ActionSelector(), + } + if domain == Platform.NUMBER: schema |= { vol.Required(CONF_STATE): selector.TemplateSelector(), @@ -435,6 +444,7 @@ TEMPLATE_TYPES = [ Platform.FAN, Platform.IMAGE, Platform.LIGHT, + Platform.LOCK, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, @@ -478,6 +488,11 @@ CONFIG_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.LIGHT), ), + Platform.LOCK: SchemaFlowFormStep( + config_schema(Platform.LOCK), + preview="template", + validate_user_input=validate_user_input(Platform.LOCK), + ), Platform.NUMBER: SchemaFlowFormStep( config_schema(Platform.NUMBER), preview="template", @@ -542,6 +557,11 @@ OPTIONS_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.LIGHT), ), + Platform.LOCK: SchemaFlowFormStep( + options_schema(Platform.LOCK), + preview="template", + validate_user_input=validate_user_input(Platform.LOCK), + ), Platform.NUMBER: SchemaFlowFormStep( options_schema(Platform.NUMBER), preview="template", @@ -578,6 +598,7 @@ CREATE_PREVIEW_ENTITY: dict[ Platform.COVER: async_create_preview_cover, Platform.FAN: async_create_preview_fan, Platform.LIGHT: async_create_preview_light, + Platform.LOCK: async_create_preview_lock, Platform.NUMBER: async_create_preview_number, Platform.SELECT: async_create_preview_select, Platform.SENSOR: async_create_preview_sensor, diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index e89f95734d1..04d26521ef1 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -15,6 +15,7 @@ from homeassistant.components.lock import ( LockEntityFeature, LockState, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, CONF_NAME, @@ -26,15 +27,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError, TemplateError from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .coordinator import TriggerUpdateCoordinator from .entity import AbstractTemplateEntity -from .helpers import async_setup_template_platform +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) from .template_entity import ( TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, @@ -82,6 +91,10 @@ PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( } ).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema) +LOCK_CONFIG_ENTRY_SCHEMA = LOCK_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + async def async_setup_platform( hass: HomeAssistant, @@ -102,6 +115,35 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + StateLockEntity, + LOCK_CONFIG_ENTRY_SCHEMA, + ) + + +@callback +def async_create_preview_lock( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateLockEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateLockEntity, + LOCK_CONFIG_ENTRY_SCHEMA, + ) + + class AbstractTemplateLock(AbstractTemplateEntity, LockEntity): """Representation of a template lock features.""" diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index cb1e26fac78..edf4516e8ab 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -188,6 +188,29 @@ }, "title": "Template light" }, + "lock": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "lock": "Actions on lock", + "unlock": "Actions on unlock", + "code_format": "[%key:component::template::common::code_format%]", + "open": "Actions on open" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "Template lock" + }, "number": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -265,6 +288,7 @@ "fan": "Template a fan", "image": "Template an image", "light": "Template a light", + "lock": "Template a lock", "number": "Template a number", "select": "Template a select", "sensor": "Template a sensor", @@ -495,6 +519,28 @@ }, "title": "[%key:component::template::config::step::light::title%]" }, + "lock": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "state": "[%key:component::template::common::state%]", + "lock": "[%key:component::template::config::step::lock::data::lock%]", + "unlock": "[%key:component::template::config::step::lock::data::unlock%]", + "code_format": "[%key:component::template::common::code_format%]", + "open": "[%key:component::template::config::step::lock::data::open%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "[%key:component::template::config::step::lock::title%]" + }, "number": { "data": { "device_id": "[%key:common::config_flow::data::device%]", diff --git a/tests/components/template/snapshots/test_lock.ambr b/tests/components/template/snapshots/test_lock.ambr new file mode 100644 index 00000000000..250fc6ba8d4 --- /dev/null +++ b/tests/components/template/snapshots/test_lock.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 9bfb0d439f7..08104025582 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -179,6 +179,16 @@ BINARY_SENSOR_OPTIONS = { {"turn_on": [], "turn_off": []}, {}, ), + ( + "lock", + {"state": "{{ states('lock.one') }}"}, + "locked", + {"one": "locked", "two": "unlocked"}, + {}, + {"lock": [], "unlock": []}, + {"lock": [], "unlock": []}, + {}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -372,6 +382,12 @@ async def test_config_flow( {"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []}, ), + ( + "lock", + {"state": "{{ states('lock.one') }}"}, + {"lock": [], "unlock": []}, + {"lock": [], "unlock": []}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -603,6 +619,16 @@ async def test_config_flow_device( {"turn_on": [], "turn_off": []}, "state", ), + ( + "lock", + {"state": "{{ states('lock.one') }}"}, + {"state": "{{ states('lock.two') }}"}, + ["locked", "unlocked"], + {"one": "locked", "two": "unlocked"}, + {"lock": [], "unlock": []}, + {"lock": [], "unlock": []}, + "state", + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -1464,6 +1490,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []}, ), + ( + "lock", + {"state": "{{ states('lock.one') }}"}, + {"lock": [], "unlock": []}, + {"lock": [], "unlock": []}, + ), ( "number", {"state": "{{ states('number.one') }}"}, diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 457c5b7bf5c..823306015bf 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant import setup from homeassistant.components import lock, template @@ -19,9 +20,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import ConfigurationStyle +from .conftest import ConfigurationStyle, async_get_flow_preview_state -from tests.common import assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component +from tests.typing import WebSocketGenerator TEST_OBJECT_ID = "test_template_lock" TEST_ENTITY_ID = f"lock.{TEST_OBJECT_ID}" @@ -1186,3 +1188,58 @@ async def test_optimistic(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.UNLOCKED + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a lock from a config entry.""" + + hass.states.async_set( + "sensor.test_state", + LockState.LOCKED, + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('sensor.test_state') }}", + "lock": [], + "unlock": [], + "template_type": lock.DOMAIN, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("lock.my_template") + assert state is not None + assert state == snapshot + + +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + lock.DOMAIN, + { + "name": "My template", + "state": "{{ 'locked' }}", + "lock": [], + "unlock": [], + }, + ) + + assert state["state"] == LockState.LOCKED From 8193259e022ac1a21c6dc87e4a238320fc3cf7af Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Jul 2025 17:06:55 +0200 Subject: [PATCH 140/265] Revert "Add select for heating circuit to Tado zones" (#149670) --- homeassistant/components/tado/__init__.py | 1 - homeassistant/components/tado/coordinator.py | 62 +- homeassistant/components/tado/select.py | 108 ---- homeassistant/components/tado/strings.json | 8 - .../tado/fixtures/heating_circuits.json | 7 - .../tado/fixtures/zone_control.json | 80 --- .../tado/snapshots/test_diagnostics.ambr | 561 ------------------ tests/components/tado/test_select.py | 91 --- tests/components/tado/util.py | 12 - 9 files changed, 3 insertions(+), 927 deletions(-) delete mode 100644 homeassistant/components/tado/select.py delete mode 100644 tests/components/tado/fixtures/heating_circuits.json delete mode 100644 tests/components/tado/fixtures/zone_control.json delete mode 100644 tests/components/tado/test_select.py diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index df33845437f..0513d63b893 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -41,7 +41,6 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.DEVICE_TRACKER, - Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.WATER_HEATER, diff --git a/homeassistant/components/tado/coordinator.py b/homeassistant/components/tado/coordinator.py index 79486ff998b..09c6ec40208 100644 --- a/homeassistant/components/tado/coordinator.py +++ b/homeassistant/components/tado/coordinator.py @@ -73,8 +73,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): "weather": {}, "geofence": {}, "zone": {}, - "zone_control": {}, - "heating_circuits": {}, } @property @@ -101,14 +99,11 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): self.home_name = tado_home["name"] devices = await self._async_update_devices() - zones, zone_controls = await self._async_update_zones() + zones = await self._async_update_zones() home = await self._async_update_home() - heating_circuits = await self._async_update_heating_circuits() self.data["device"] = devices self.data["zone"] = zones - self.data["zone_control"] = zone_controls - self.data["heating_circuits"] = heating_circuits self.data["weather"] = home["weather"] self.data["geofence"] = home["geofence"] @@ -171,7 +166,7 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): return mapped_devices - async def _async_update_zones(self) -> tuple[dict[int, dict], dict[int, dict]]: + async def _async_update_zones(self) -> dict[int, dict]: """Update the zone data from Tado.""" try: @@ -184,12 +179,10 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): raise UpdateFailed(f"Error updating Tado zones: {err}") from err mapped_zones: dict[int, dict] = {} - mapped_zone_controls: dict[int, dict] = {} for zone in zone_states: mapped_zones[int(zone)] = await self._update_zone(int(zone)) - mapped_zone_controls[int(zone)] = await self._update_zone_control(int(zone)) - return mapped_zones, mapped_zone_controls + return mapped_zones async def _update_zone(self, zone_id: int) -> dict[str, str]: """Update the internal data of a zone.""" @@ -206,24 +199,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): _LOGGER.debug("Zone %s updated, with data: %s", zone_id, data) return data - async def _update_zone_control(self, zone_id: int) -> dict[str, Any]: - """Update the internal zone control data of a zone.""" - - _LOGGER.debug("Updating zone control for zone %s", zone_id) - try: - zone_control_data = await self.hass.async_add_executor_job( - self._tado.get_zone_control, zone_id - ) - except RequestException as err: - _LOGGER.error( - "Error updating Tado zone control for zone %s: %s", zone_id, err - ) - raise UpdateFailed( - f"Error updating Tado zone control for zone {zone_id}: {err}" - ) from err - - return zone_control_data - async def _async_update_home(self) -> dict[str, dict]: """Update the home data from Tado.""" @@ -242,23 +217,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): return {"weather": weather, "geofence": geofence} - async def _async_update_heating_circuits(self) -> dict[str, dict]: - """Update the heating circuits data from Tado.""" - - try: - heating_circuits = await self.hass.async_add_executor_job( - self._tado.get_heating_circuits - ) - except RequestException as err: - _LOGGER.error("Error updating Tado heating circuits: %s", err) - raise UpdateFailed(f"Error updating Tado heating circuits: {err}") from err - - mapped_heating_circuits: dict[str, dict] = {} - for circuit in heating_circuits: - mapped_heating_circuits[circuit["driverShortSerialNo"]] = circuit - - return mapped_heating_circuits - async def get_capabilities(self, zone_id: int | str) -> dict: """Fetch the capabilities from Tado.""" @@ -406,20 +364,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): except RequestException as exc: raise HomeAssistantError(f"Error setting Tado child lock: {exc}") from exc - async def set_heating_circuit(self, zone_id: int, circuit_id: int | None) -> None: - """Set heating circuit for zone.""" - try: - await self.hass.async_add_executor_job( - self._tado.set_zone_heating_circuit, - zone_id, - circuit_id, - ) - except RequestException as exc: - raise HomeAssistantError( - f"Error setting Tado heating circuit: {exc}" - ) from exc - await self._update_zone_control(zone_id) - class TadoMobileDeviceUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): """Class to manage the mobile devices from Tado via PyTado.""" diff --git a/homeassistant/components/tado/select.py b/homeassistant/components/tado/select.py deleted file mode 100644 index 6db765128c2..00000000000 --- a/homeassistant/components/tado/select.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Module for Tado select entities.""" - -import logging - -from homeassistant.components.select import SelectEntity -from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from . import TadoConfigEntry -from .entity import TadoDataUpdateCoordinator, TadoZoneEntity - -_LOGGER = logging.getLogger(__name__) - -NO_HEATING_CIRCUIT_OPTION = "no_heating_circuit" - - -async def async_setup_entry( - hass: HomeAssistant, - entry: TadoConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up the Tado select platform.""" - - tado = entry.runtime_data.coordinator - entities: list[SelectEntity] = [ - TadoHeatingCircuitSelectEntity(tado, zone["name"], zone["id"]) - for zone in tado.zones - if zone["type"] == "HEATING" - ] - - async_add_entities(entities, True) - - -class TadoHeatingCircuitSelectEntity(TadoZoneEntity, SelectEntity): - """Representation of a Tado heating circuit select entity.""" - - _attr_entity_category = EntityCategory.CONFIG - _attr_has_entity_name = True - _attr_icon = "mdi:water-boiler" - _attr_translation_key = "heating_circuit" - - def __init__( - self, - coordinator: TadoDataUpdateCoordinator, - zone_name: str, - zone_id: int, - ) -> None: - """Initialize the Tado heating circuit select entity.""" - super().__init__(zone_name, coordinator.home_id, zone_id, coordinator) - - self._attr_unique_id = f"{zone_id} {coordinator.home_id} heating_circuit" - - self._attr_options = [] - self._attr_current_option = None - - async def async_select_option(self, option: str) -> None: - """Update the selected heating circuit.""" - heating_circuit_id = ( - None - if option == NO_HEATING_CIRCUIT_OPTION - else self.coordinator.data["heating_circuits"].get(option, {}).get("number") - ) - await self.coordinator.set_heating_circuit(self.zone_id, heating_circuit_id) - await self.coordinator.async_request_refresh() - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_callback() - super()._handle_coordinator_update() - - @callback - def _async_update_callback(self) -> None: - """Handle update callbacks.""" - # Heating circuits list - heating_circuits = self.coordinator.data["heating_circuits"].values() - self._attr_options = [NO_HEATING_CIRCUIT_OPTION] - self._attr_options.extend(hc["driverShortSerialNo"] for hc in heating_circuits) - - # Current heating circuit - zone_control = self.coordinator.data["zone_control"].get(self.zone_id) - if zone_control and "heatingCircuit" in zone_control: - heating_circuit_number = zone_control["heatingCircuit"] - if heating_circuit_number is None: - self._attr_current_option = NO_HEATING_CIRCUIT_OPTION - else: - # Find heating circuit by number - heating_circuit = next( - ( - hc - for hc in heating_circuits - if hc.get("number") == heating_circuit_number - ), - None, - ) - - if heating_circuit is None: - _LOGGER.error( - "Heating circuit with number %s not found for zone %s", - heating_circuit_number, - self.zone_name, - ) - self._attr_current_option = NO_HEATING_CIRCUIT_OPTION - else: - self._attr_current_option = heating_circuit.get( - "driverShortSerialNo" - ) diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index ba1c9e95683..5d9c4237be8 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -59,14 +59,6 @@ } } }, - "select": { - "heating_circuit": { - "name": "Heating circuit", - "state": { - "no_heating_circuit": "No circuit" - } - } - }, "switch": { "child_lock": { "name": "Child lock" diff --git a/tests/components/tado/fixtures/heating_circuits.json b/tests/components/tado/fixtures/heating_circuits.json deleted file mode 100644 index 723ceb76f95..00000000000 --- a/tests/components/tado/fixtures/heating_circuits.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "number": 1, - "driverSerialNo": "RU1234567890", - "driverShortSerialNo": "RU1234567890" - } -] diff --git a/tests/components/tado/fixtures/zone_control.json b/tests/components/tado/fixtures/zone_control.json deleted file mode 100644 index 584fe9f3c92..00000000000 --- a/tests/components/tado/fixtures/zone_control.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "type": "HEATING", - "earlyStartEnabled": false, - "heatingCircuit": 1, - "duties": { - "type": "HEATING", - "leader": { - "deviceType": "RU01", - "serialNo": "RU1234567890", - "shortSerialNo": "RU1234567890", - "currentFwVersion": "54.20", - "connectionState": { - "value": true, - "timestamp": "2025-06-30T19:53:40.710Z" - }, - "characteristics": { - "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] - }, - "batteryState": "NORMAL" - }, - "drivers": [ - { - "deviceType": "VA01", - "serialNo": "VA1234567890", - "shortSerialNo": "VA1234567890", - "currentFwVersion": "54.20", - "connectionState": { - "value": true, - "timestamp": "2025-06-30T19:54:15.166Z" - }, - "characteristics": { - "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] - }, - "mountingState": { - "value": "CALIBRATED", - "timestamp": "2025-06-09T23:25:12.678Z" - }, - "mountingStateWithError": "CALIBRATED", - "batteryState": "LOW", - "childLockEnabled": false - } - ], - "uis": [ - { - "deviceType": "RU01", - "serialNo": "RU1234567890", - "shortSerialNo": "RU1234567890", - "currentFwVersion": "54.20", - "connectionState": { - "value": true, - "timestamp": "2025-06-30T19:53:40.710Z" - }, - "characteristics": { - "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] - }, - "batteryState": "NORMAL" - }, - { - "deviceType": "VA01", - "serialNo": "VA1234567890", - "shortSerialNo": "VA1234567890", - "currentFwVersion": "54.20", - "connectionState": { - "value": true, - "timestamp": "2025-06-30T19:54:15.166Z" - }, - "characteristics": { - "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] - }, - "mountingState": { - "value": "CALIBRATED", - "timestamp": "2025-06-09T23:25:12.678Z" - }, - "mountingStateWithError": "CALIBRATED", - "batteryState": "LOW", - "childLockEnabled": false - } - ] - } -} diff --git a/tests/components/tado/snapshots/test_diagnostics.ambr b/tests/components/tado/snapshots/test_diagnostics.ambr index 34d26c222fa..eefb818a88c 100644 --- a/tests/components/tado/snapshots/test_diagnostics.ambr +++ b/tests/components/tado/snapshots/test_diagnostics.ambr @@ -62,13 +62,6 @@ 'presence': 'HOME', 'presenceLocked': False, }), - 'heating_circuits': dict({ - 'RU1234567890': dict({ - 'driverSerialNo': 'RU1234567890', - 'driverShortSerialNo': 'RU1234567890', - 'number': 1, - }), - }), 'weather': dict({ 'outsideTemperature': dict({ 'celsius': 7.46, @@ -117,560 +110,6 @@ 'repr': "TadoZone(zone_id=6, current_temp=24.3, connection=None, current_temp_timestamp='2024-06-28T22: 23: 15.679Z', current_humidity=70.9, current_humidity_timestamp='2024-06-28T22: 23: 15.679Z', is_away=False, current_hvac_action='HEATING', current_fan_speed='AUTO', current_fan_level='LEVEL3', current_hvac_mode='HEAT', current_swing_mode='OFF', current_vertical_swing_mode='ON', current_horizontal_swing_mode='ON', target_temp=25.0, available=True, power='ON', link='ONLINE', ac_power_timestamp='2022-07-13T18: 06: 58.183Z', heating_power_timestamp=None, ac_power='ON', heating_power=None, heating_power_percentage=None, tado_mode='HOME', overlay_termination_type='MANUAL', overlay_termination_timestamp=None, default_overlay_termination_type='MANUAL', default_overlay_termination_duration=None, preparation=False, open_window=False, open_window_detected=False, open_window_attr={}, precision=0.1)", }), }), - 'zone_control': dict({ - '1': dict({ - 'duties': dict({ - 'drivers': list([ - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - 'leader': dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - 'type': 'HEATING', - 'uis': list([ - dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - }), - 'earlyStartEnabled': False, - 'heatingCircuit': 1, - 'type': 'HEATING', - }), - '2': dict({ - 'duties': dict({ - 'drivers': list([ - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - 'leader': dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - 'type': 'HEATING', - 'uis': list([ - dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - }), - 'earlyStartEnabled': False, - 'heatingCircuit': 1, - 'type': 'HEATING', - }), - '3': dict({ - 'duties': dict({ - 'drivers': list([ - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - 'leader': dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - 'type': 'HEATING', - 'uis': list([ - dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - }), - 'earlyStartEnabled': False, - 'heatingCircuit': 1, - 'type': 'HEATING', - }), - '4': dict({ - 'duties': dict({ - 'drivers': list([ - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - 'leader': dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - 'type': 'HEATING', - 'uis': list([ - dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - }), - 'earlyStartEnabled': False, - 'heatingCircuit': 1, - 'type': 'HEATING', - }), - '5': dict({ - 'duties': dict({ - 'drivers': list([ - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - 'leader': dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - 'type': 'HEATING', - 'uis': list([ - dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - }), - 'earlyStartEnabled': False, - 'heatingCircuit': 1, - 'type': 'HEATING', - }), - '6': dict({ - 'duties': dict({ - 'drivers': list([ - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - 'leader': dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - 'type': 'HEATING', - 'uis': list([ - dict({ - 'batteryState': 'NORMAL', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:53:40.710Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'RU01', - 'serialNo': 'RU1234567890', - 'shortSerialNo': 'RU1234567890', - }), - dict({ - 'batteryState': 'LOW', - 'characteristics': dict({ - 'capabilities': list([ - 'INSIDE_TEMPERATURE_MEASUREMENT', - 'IDENTIFY', - ]), - }), - 'childLockEnabled': False, - 'connectionState': dict({ - 'timestamp': '2025-06-30T19:54:15.166Z', - 'value': True, - }), - 'currentFwVersion': '54.20', - 'deviceType': 'VA01', - 'mountingState': dict({ - 'timestamp': '2025-06-09T23:25:12.678Z', - 'value': 'CALIBRATED', - }), - 'mountingStateWithError': 'CALIBRATED', - 'serialNo': 'VA1234567890', - 'shortSerialNo': 'VA1234567890', - }), - ]), - }), - 'earlyStartEnabled': False, - 'heatingCircuit': 1, - 'type': 'HEATING', - }), - }), }), 'mobile_devices': dict({ 'mobile_device': dict({ diff --git a/tests/components/tado/test_select.py b/tests/components/tado/test_select.py deleted file mode 100644 index e57b7510d1b..00000000000 --- a/tests/components/tado/test_select.py +++ /dev/null @@ -1,91 +0,0 @@ -"""The select tests for the tado platform.""" - -from unittest.mock import patch - -import pytest - -from homeassistant.components.select import ( - DOMAIN as SELECT_DOMAIN, - SERVICE_SELECT_OPTION, -) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION -from homeassistant.core import HomeAssistant - -from .util import async_init_integration - -HEATING_CIRCUIT_SELECT_ENTITY = "select.baseboard_heater_heating_circuit" -NO_HEATING_CIRCUIT = "no_heating_circuit" -HEATING_CIRCUIT_OPTION = "RU1234567890" -ZONE_ID = 1 -HEATING_CIRCUIT_ID = 1 - - -async def test_heating_circuit_select(hass: HomeAssistant) -> None: - """Test creation of heating circuit select entity.""" - - await async_init_integration(hass) - state = hass.states.get(HEATING_CIRCUIT_SELECT_ENTITY) - assert state is not None - assert state.state == HEATING_CIRCUIT_OPTION - assert NO_HEATING_CIRCUIT in state.attributes["options"] - assert HEATING_CIRCUIT_OPTION in state.attributes["options"] - - -@pytest.mark.parametrize( - ("option", "expected_circuit_id"), - [(HEATING_CIRCUIT_OPTION, HEATING_CIRCUIT_ID), (NO_HEATING_CIRCUIT, None)], -) -async def test_heating_circuit_select_action( - hass: HomeAssistant, option, expected_circuit_id -) -> None: - """Test selecting heating circuit option.""" - - await async_init_integration(hass) - - # Test selecting a specific heating circuit - with ( - patch( - "homeassistant.components.tado.PyTado.interface.api.Tado.set_zone_heating_circuit" - ) as mock_set_zone_heating_circuit, - patch( - "homeassistant.components.tado.PyTado.interface.api.Tado.get_zone_control" - ) as mock_get_zone_control, - ): - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: HEATING_CIRCUIT_SELECT_ENTITY, - ATTR_OPTION: option, - }, - blocking=True, - ) - - mock_set_zone_heating_circuit.assert_called_with(ZONE_ID, expected_circuit_id) - assert mock_get_zone_control.called - - -@pytest.mark.usefixtures("caplog") -async def test_heating_circuit_not_found( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test when a heating circuit with a specific number is not found.""" - circuit_not_matching_zone_control = 999 - heating_circuits = [ - { - "number": circuit_not_matching_zone_control, - "driverSerialNo": "RU1234567890", - "driverShortSerialNo": "RU1234567890", - } - ] - - with patch( - "homeassistant.components.tado.PyTado.interface.api.Tado.get_heating_circuits", - return_value=heating_circuits, - ): - await async_init_integration(hass) - - state = hass.states.get(HEATING_CIRCUIT_SELECT_ENTITY) - assert state.state == NO_HEATING_CIRCUIT - - assert "Heating circuit with number 1 not found for zone" in caplog.text diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index 5ef0ab5dbf2..8ee7209acb2 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -20,10 +20,8 @@ async def async_init_integration( me_fixture = "me.json" weather_fixture = "weather.json" home_fixture = "home.json" - home_heating_circuits_fixture = "heating_circuits.json" home_state_fixture = "home_state.json" zones_fixture = "zones.json" - zone_control_fixture = "zone_control.json" zone_states_fixture = "zone_states.json" # WR1 Device @@ -72,10 +70,6 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/", text=await async_load_fixture(hass, home_fixture, DOMAIN), ) - m.get( - "https://my.tado.com/api/v2/homes/1/heatingCircuits", - text=await async_load_fixture(hass, home_heating_circuits_fixture, DOMAIN), - ) m.get( "https://my.tado.com/api/v2/homes/1/weather", text=await async_load_fixture(hass, weather_fixture, DOMAIN), @@ -184,12 +178,6 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zones/1/state", text=await async_load_fixture(hass, zone_1_state_fixture, DOMAIN), ) - zone_ids = [1, 2, 3, 4, 5, 6] - for zone_id in zone_ids: - m.get( - f"https://my.tado.com/api/v2/homes/1/zones/{zone_id}/control", - text=await async_load_fixture(hass, zone_control_fixture, DOMAIN), - ) m.post( "https://login.tado.com/oauth2/token", text=await async_load_fixture(hass, token_fixture, DOMAIN), From 8114df42192faac7af9e640db0a16deae37a6255 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Jul 2025 18:36:20 +0200 Subject: [PATCH 141/265] Bump version to 2025.9.0 (#149680) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ce7cf1ac124..e96de66ac76 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,7 +40,7 @@ env: CACHE_VERSION: 4 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 - HA_SHORT_VERSION: "2025.8" + HA_SHORT_VERSION: "2025.9" DEFAULT_PYTHON: "3.13" ALL_PYTHON_VERSIONS: "['3.13']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index 2daa6d91db2..1983932813e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 -MINOR_VERSION: Final = 8 +MINOR_VERSION: Final = 9 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index 35a2bf2c7fb..71182e99560 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0.dev0" +version = "2025.9.0.dev0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 736d582d049a2d02b7c494c23ffbd529ab6bc64f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 30 Jul 2025 18:53:21 +0200 Subject: [PATCH 142/265] Fix translation string reference for MQTT climate subentry option (#149673) --- homeassistant/components/mqtt/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 22fb85780b0..c14bda008d1 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -426,7 +426,7 @@ }, "data_description": { "payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]", - "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]", + "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_on%]", "power_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".", "power_command_topic": "The MQTT topic to publish commands to change the climate power state. Sends the payload configured with payload \"on\" or payload \"off\". [Learn more.]({url}#power_command_topic)" } @@ -812,7 +812,7 @@ "min_humidity": "The minimum target humidity that can be set.", "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the humidity command topic.", "target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)", - "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the humidity state topic with.", + "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.", "target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)" } }, From 09b91bd76afac3fa90ddad4f54bbb0f9611999f0 Mon Sep 17 00:00:00 2001 From: Renat Sibgatulin Date: Wed, 30 Jul 2025 19:48:36 +0200 Subject: [PATCH 143/265] Clean airq tests (#149682) --- tests/components/airq/common.py | 18 ++++++++++++++++++ tests/components/airq/test_config_flow.py | 17 ++++------------- tests/components/airq/test_coordinator.py | 12 ++---------- 3 files changed, 24 insertions(+), 23 deletions(-) create mode 100644 tests/components/airq/common.py diff --git a/tests/components/airq/common.py b/tests/components/airq/common.py new file mode 100644 index 00000000000..275da60e3a2 --- /dev/null +++ b/tests/components/airq/common.py @@ -0,0 +1,18 @@ +"""Common methods used across tests for air-Q.""" + +from aioairq import DeviceInfo + +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD + +TEST_USER_DATA = { + CONF_IP_ADDRESS: "192.168.0.0", + CONF_PASSWORD: "password", +} +TEST_DEVICE_INFO = DeviceInfo( + id="id", + name="name", + model="model", + sw_version="sw", + hw_version="hw", +) +TEST_DEVICE_DATA = {"co2": 500.0, "Status": "OK"} diff --git a/tests/components/airq/test_config_flow.py b/tests/components/airq/test_config_flow.py index 09da6343e05..95c22cb12c8 100644 --- a/tests/components/airq/test_config_flow.py +++ b/tests/components/airq/test_config_flow.py @@ -3,7 +3,7 @@ import logging from unittest.mock import patch -from aioairq import DeviceInfo, InvalidAuth +from aioairq import InvalidAuth from aiohttp.client_exceptions import ClientConnectionError import pytest @@ -13,25 +13,16 @@ from homeassistant.components.airq.const import ( CONF_RETURN_AVERAGE, DOMAIN, ) -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .common import TEST_DEVICE_INFO, TEST_USER_DATA + from tests.common import MockConfigEntry pytestmark = pytest.mark.usefixtures("mock_setup_entry") -TEST_USER_DATA = { - CONF_IP_ADDRESS: "192.168.0.0", - CONF_PASSWORD: "password", -} -TEST_DEVICE_INFO = DeviceInfo( - id="id", - name="name", - model="model", - sw_version="sw", - hw_version="hw", -) DEFAULT_OPTIONS = { CONF_CLIP_NEGATIVE: True, CONF_RETURN_AVERAGE: True, diff --git a/tests/components/airq/test_coordinator.py b/tests/components/airq/test_coordinator.py index 69f7c9dee17..6512d60ddbe 100644 --- a/tests/components/airq/test_coordinator.py +++ b/tests/components/airq/test_coordinator.py @@ -3,7 +3,6 @@ import logging from unittest.mock import patch -from aioairq import DeviceInfo as AirQDeviceInfo import pytest from homeassistant.components.airq import AirQCoordinator @@ -12,9 +11,10 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo +from .common import TEST_DEVICE_DATA, TEST_DEVICE_INFO + from tests.common import MockConfigEntry -pytestmark = pytest.mark.usefixtures("mock_setup_entry") MOCKED_ENTRY = MockConfigEntry( domain=DOMAIN, data={ @@ -24,14 +24,6 @@ MOCKED_ENTRY = MockConfigEntry( unique_id="123-456", ) -TEST_DEVICE_INFO = AirQDeviceInfo( - id="id", - name="name", - model="model", - sw_version="sw", - hw_version="hw", -) -TEST_DEVICE_DATA = {"co2": 500.0, "Status": "OK"} STATUS_WARMUP = { "co": "co sensor still in warm up phase; waiting time = 18 s", "tvoc": "tvoc sensor still in warm up phase; waiting time = 18 s", From a76af50c10da2ce258a15e8026606365b4932171 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 30 Jul 2025 12:57:59 -0500 Subject: [PATCH 144/265] Bump intents to 2025.7.30 (#149678) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- tests/components/assist_pipeline/test_pipeline.py | 4 ++-- tests/components/conversation/snapshots/test_http.ambr | 8 ++++---- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ad0a4c96102..31adffad064 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.23"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 819bb2f5c9a..704fb282784 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.110.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250730.0 -home-assistant-intents==2025.6.23 +home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/requirements_all.txt b/requirements_all.txt index f731ecc0e0d..c01d9eef347 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1177,7 +1177,7 @@ holidays==0.77 home-assistant-frontend==20250730.0 # homeassistant.components.conversation -home-assistant-intents==2025.6.23 +home-assistant-intents==2025.7.30 # homeassistant.components.homematicip_cloud homematicip==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64931e1ef4e..eada71b4f02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1026,7 +1026,7 @@ holidays==0.77 home-assistant-frontend==20250730.0 # homeassistant.components.conversation -home-assistant-intents==2025.6.23 +home-assistant-intents==2025.7.30 # homeassistant.components.homematicip_cloud homematicip==2.2.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 5168388c934..5776f6dfe12 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -32,7 +32,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ hassil==2.2.3 \ - home-assistant-intents==2025.6.23 \ + home-assistant-intents==2025.7.30 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ pyspeex-noise==1.0.2 diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 5bc7b86c38c..0cb67302700 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -375,7 +375,7 @@ async def test_get_pipelines(hass: HomeAssistant) -> None: ("en", "us", "en", "en"), ("en", "uk", "en", "en"), ("pt", "pt", "pt", "pt"), - ("pt", "br", "pt-br", "pt"), + ("pt", "br", "pt-BR", "pt"), ], ) async def test_default_pipeline_no_stt_tts( @@ -428,7 +428,7 @@ async def test_default_pipeline_no_stt_tts( ("en", "us", "en", "en", "en", "en"), ("en", "uk", "en", "en", "en", "en"), ("pt", "pt", "pt", "pt", "pt", "pt"), - ("pt", "br", "pt-br", "pt", "pt-br", "pt-br"), + ("pt", "br", "pt-BR", "pt", "pt-br", "pt-br"), ], ) @pytest.mark.usefixtures("init_supporting_components") diff --git a/tests/components/conversation/snapshots/test_http.ambr b/tests/components/conversation/snapshots/test_http.ambr index 391fb609d65..8f68274d37f 100644 --- a/tests/components/conversation/snapshots/test_http.ambr +++ b/tests/components/conversation/snapshots/test_http.ambr @@ -45,7 +45,7 @@ 'nl', 'pl', 'pt', - 'pt-br', + 'pt-BR', 'ro', 'ru', 'sk', @@ -60,9 +60,9 @@ 'uk', 'ur', 'vi', - 'zh-cn', - 'zh-hk', - 'zh-tw', + 'zh-CN', + 'zh-HK', + 'zh-TW', ]), }), dict({ From 8d27ca1e21547978a38d702346a9089e80204d10 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:59:01 +0200 Subject: [PATCH 145/265] Fix `KeyError` in friends coordinator (#149684) --- .../components/playstation_network/coordinator.py | 8 +++++--- tests/components/playstation_network/conftest.py | 8 ++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/playstation_network/coordinator.py b/homeassistant/components/playstation_network/coordinator.py index c447e8dc503..977632de23b 100644 --- a/homeassistant/components/playstation_network/coordinator.py +++ b/homeassistant/components/playstation_network/coordinator.py @@ -6,7 +6,7 @@ from abc import abstractmethod from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from psnawp_api.core.psnawp_exceptions import ( PSNAWPAuthenticationError, @@ -29,7 +29,7 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_ACCOUNT_ID, DOMAIN +from .const import DOMAIN from .helpers import PlaystationNetwork, PlaystationNetworkData _LOGGER = logging.getLogger(__name__) @@ -176,7 +176,9 @@ class PlaystationNetworkFriendDataCoordinator( def _setup(self) -> None: """Set up the coordinator.""" - self.user = self.psn.psn.user(account_id=self.subentry.data[CONF_ACCOUNT_ID]) + if TYPE_CHECKING: + assert self.subentry.unique_id + self.user = self.psn.psn.user(account_id=self.subentry.unique_id) self.profile = self.user.profile() async def _async_setup(self) -> None: diff --git a/tests/components/playstation_network/conftest.py b/tests/components/playstation_network/conftest.py index ab4edc0e3f4..bfbdc9a72bd 100644 --- a/tests/components/playstation_network/conftest.py +++ b/tests/components/playstation_network/conftest.py @@ -14,11 +14,7 @@ from psnawp_api.models.trophies import ( ) import pytest -from homeassistant.components.playstation_network.const import ( - CONF_ACCOUNT_ID, - CONF_NPSSO, - DOMAIN, -) +from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN from homeassistant.config_entries import ConfigSubentryData from tests.common import MockConfigEntry @@ -40,7 +36,7 @@ def mock_config_entry() -> MockConfigEntry: unique_id=PSN_ID, subentries_data=[ ConfigSubentryData( - data={CONF_ACCOUNT_ID: "fren-psn-id"}, + data={}, subentry_id="ABCDEF", subentry_type="friend", title="PublicUniversalFriend", From 389a1251a140b79c6bdad587930eccf514d488b2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:59:41 -0400 Subject: [PATCH 146/265] Bump ZHA to 0.0.64 (#149683) Co-authored-by: TheJulianJES Co-authored-by: abmantis --- homeassistant/components/zha/helpers.py | 22 +++++++++++++++- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/strings.json | 30 ++++++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_update.py | 25 +++++++++++++++++- 6 files changed, 78 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 084e1c882ac..f5b44eb8fc4 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -74,7 +74,12 @@ from zha.event import EventBase from zha.exceptions import ZHAException from zha.mixins import LogMixin from zha.zigbee.cluster_handlers import ClusterBindEvent, ClusterConfigureReportingEvent -from zha.zigbee.device import ClusterHandlerConfigurationComplete, Device, ZHAEvent +from zha.zigbee.device import ( + ClusterHandlerConfigurationComplete, + Device, + DeviceFirmwareInfoUpdatedEvent, + ZHAEvent, +) from zha.zigbee.group import Group, GroupInfo, GroupMember from zigpy.config import ( CONF_DATABASE, @@ -843,8 +848,23 @@ class ZHAGatewayProxy(EventBase): name=zha_device.name, manufacturer=zha_device.manufacturer, model=zha_device.model, + sw_version=zha_device.firmware_version, ) zha_device_proxy.device_id = device_registry_device.id + + def update_sw_version(event: DeviceFirmwareInfoUpdatedEvent) -> None: + """Update software version in device registry.""" + device_registry.async_update_device( + device_registry_device.id, + sw_version=event.new_firmware_version, + ) + + self._unsubs.append( + zha_device.on_event( + DeviceFirmwareInfoUpdatedEvent.event_type, update_sw_version + ) + ) + return zha_device_proxy def _async_get_or_create_group_proxy(self, group_info: GroupInfo) -> ZHAGroupProxy: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 2cbc962a305..ec08c4f5d9d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.62"], + "requirements": ["zha==0.0.64"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 23d17ea128f..1c9454ec0a0 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -616,6 +616,18 @@ }, "water_supply": { "name": "Water supply" + }, + "frient_in_1": { + "name": "IN1" + }, + "frient_in_2": { + "name": "IN2" + }, + "frient_in_3": { + "name": "IN3" + }, + "frient_in_4": { + "name": "IN4" } }, "button": { @@ -639,6 +651,9 @@ }, "frost_lock_reset": { "name": "Frost lock reset" + }, + "reset_alarm": { + "name": "Reset alarm" } }, "climate": { @@ -1472,6 +1487,9 @@ "tier6_summation_delivered": { "name": "Tier 6 summation delivered" }, + "total_active_power": { + "name": "Total power" + }, "summation_received": { "name": "Summation received" }, @@ -2006,6 +2024,18 @@ }, "auto_relock": { "name": "Autorelock" + }, + "distance_tracking": { + "name": "Distance tracking" + }, + "water_shortage_auto_close": { + "name": "Water shortage auto-close" + }, + "frient_com_1": { + "name": "COM 1" + }, + "frient_com_2": { + "name": "COM 2" } } } diff --git a/requirements_all.txt b/requirements_all.txt index c01d9eef347..f5f0c5116dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3203,7 +3203,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.62 +zha==0.0.64 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eada71b4f02..9336bbcc68c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2647,7 +2647,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.62 +zha==0.0.64 # homeassistant.components.zwave_js zwave-js-server-python==0.67.0 diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index c8cbc407106..04d190b170c 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -47,6 +47,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .common import find_entity_id, update_attribute_cache @@ -156,7 +157,6 @@ async def setup_test_data( ) ) zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) - zha_device_proxy.device.async_update_sw_build_id(installed_fw_version) return zha_device_proxy, cluster, fw_image, installed_fw_version @@ -643,3 +643,26 @@ async def test_update_release_notes( assert "Some lengthy release notes" in result["result"] assert OTA_MESSAGE_RELIABILITY in result["result"] assert OTA_MESSAGE_BATTERY_POWERED in result["result"] + + +async def test_update_version_sync_device_registry( + hass: HomeAssistant, + setup_zha, + zigpy_device_mock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test firmware version syncing between the ZHA device and Home Assistant.""" + await setup_zha() + zha_device, _, _, _ = await setup_test_data(hass, zigpy_device_mock) + + zha_device.device.async_update_firmware_version("0x12345678") + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.device.ieee))} + ) + assert reg_device.sw_version == "0x12345678" + + zha_device.device.async_update_firmware_version("0xabcd1234") + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.device.ieee))} + ) + assert reg_device.sw_version == "0xabcd1234" From 1ead01bc9a85df0917f3a5a68be9c68408580818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 30 Jul 2025 20:19:01 +0200 Subject: [PATCH 147/265] Explicitly pass config_entry to miele coordinator (#149691) --- homeassistant/components/miele/__init__.py | 2 +- homeassistant/components/miele/coordinator.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/miele/__init__.py b/homeassistant/components/miele/__init__.py index 1cb2fc0fab1..2c5c250aee7 100644 --- a/homeassistant/components/miele/__init__.py +++ b/homeassistant/components/miele/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo ) from err # Setup MieleAPI and coordinator for data fetch - coordinator = MieleDataUpdateCoordinator(hass, auth) + coordinator = MieleDataUpdateCoordinator(hass, entry, auth) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index 27456ffe04c..d5de2d79cb9 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -42,12 +42,14 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]): def __init__( self, hass: HomeAssistant, + config_entry: MieleConfigEntry, api: AsyncConfigEntryAuth, ) -> None: """Initialize the Miele data coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=120), ) From b4e50902ebed2152a7eef4164112c3034a1a3c1d Mon Sep 17 00:00:00 2001 From: Roman Sivriver Date: Wed, 30 Jul 2025 17:29:26 -0400 Subject: [PATCH 148/265] Fix typo in backup log message (#149705) --- homeassistant/components/backup/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index e7fc1262f6d..f1b2f7d5b97 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -1119,7 +1119,7 @@ class BackupManager: ) if unavailable_agents: LOGGER.warning( - "Backup agents %s are not available, will backupp to %s", + "Backup agents %s are not available, will backup to %s", unavailable_agents, available_agents, ) From 2706c7d67d27e3a839e9eb890a6b93e8ee054c05 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:30:05 -0400 Subject: [PATCH 149/265] Add translations for all fields in template integration (#149692) Co-authored-by: Norbert Rittel --- .../components/template/strings.json | 238 +++++++++++++++--- 1 file changed, 209 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index edf4516e8ab..b412fa519cd 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -2,6 +2,7 @@ "common": { "advanced_options": "Advanced options", "availability": "Availability template", + "availability_description": "Defines a template to get the `available` state of the entity. If the template either fails to render or returns `True`, `\"1\"`, `\"true\"`, `\"yes\"`, `\"on\"`, `\"enable\"`, or a non-zero number, the entity will be `available`. If the template returns any other value, the entity will be `unavailable`. If not configured, the entity will always be `available`. Note that the string comparison is not case sensitive; `\"TrUe\"` and `\"yEs\"` are allowed.", "code_format": "Code format", "device_class": "Device class", "device_id_description": "Select a device to link to this entity.", @@ -28,13 +29,26 @@ "code_format": "[%key:component::template::common::code_format%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "value_template": "Defines a template to set the state of the alarm panel. Valid output values from the template are `armed_away`, `armed_home`, `armed_night`, `armed_vacation`, `arming`, `disarmed`, `pending`, and `triggered`.", + "disarm": "Defines actions to run when the alarm control panel is disarmed. Receives variable `code`.", + "arm_away": "Defines actions to run when the alarm control panel is armed to `arm_away`. Receives variable `code`.", + "arm_custom_bypass": "Defines actions to run when the alarm control panel is armed to `arm_custom_bypass`. Receives variable `code`.", + "arm_home": "Defines actions to run when the alarm control panel is armed to `arm_home`. Receives variable `code`.", + "arm_night": "Defines actions to run when the alarm control panel is armed to `arm_night`. Receives variable `code`.", + "arm_vacation": "Defines actions to run when the alarm control panel is armed to `arm_vacation`. Receives variable `code`.", + "trigger": "Defines actions to run when the alarm control panel is triggered. Receives variable `code`.", + "code_arm_required": "If true, the code is required to arm the alarm.", + "code_format": "One of number, text or no_code. Format for the code used to arm/disarm the alarm." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -48,13 +62,17 @@ "state": "[%key:component::template::common::state%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "The sensor is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -68,13 +86,17 @@ "press": "Actions on press" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "press": "Defines actions to run when button is pressed." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -99,13 +121,16 @@ "close_cover": "Defines actions to run when the cover is closed.", "stop_cover": "Defines actions to run when the cover is stopped.", "position": "Defines a template to get the position of the cover. Value values are numbers between `0` (`closed`) and `100` (`open`).", - "set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command." + "set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command. Receives variable `position`." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -124,11 +149,11 @@ }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", - "state": "Defines a template to get the state of the fan. Valid values: `on`, `off`.", + "state": "The fan is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.", "turn_off": "Defines actions to run when the fan is turned off.", - "turn_on": "Defines actions to run when the fan is turned on.", + "turn_on": "Defines actions to run when the fan is turned on. Receives variables `percentage` and/or `preset_mode`.", "percentage": "Defines a template to get the speed percentage of the fan.", - "set_percentage": "Defines actions to run when the fan is given a speed percentage command.", + "set_percentage": "Defines actions to run when the fan is given a speed percentage command. Receives variable `percentage`.", "speed_count": "The number of speeds the fan supports. Used to calculate the percentage step for the `fan.increase_speed` and `fan.decrease_speed` actions." }, "sections": { @@ -136,6 +161,9 @@ "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -149,13 +177,18 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "url": "Defines a template to get the URL on which the image is served.", + "verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http-only URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -176,13 +209,25 @@ "set_temperature": "Actions on set color temperature" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "The light is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.", + "turn_off": "Defines actions to run when the light is turned off.", + "turn_on": "Defines actions to run when the light is turned on.", + "level": "Defines a template to get the brightness of the light. Valid values are 0 to 255.", + "set_level": "Defines actions to run when the light is given a brightness command. The script will only be called if the `turn_on` call only has `brightness`, and optionally `transition`. Receives variables `brightness` and, optionally, `transition`.", + "hs": "Defines a template to get the HS color of the light. Must render a tuple (hue, saturation).", + "set_hs": "Defines actions to run when the light is given a hs color command. Available variables: `hs` as a tuple, `h` and `s`.", + "temperature": "Defines a template to get the color temperature of the light.", + "set_temperature": "Defines actions to run when the light is given a color temperature command. Receives variable `color_temp_kelvin`. May also receive variables `brightness` and/or `transition`." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -199,13 +244,21 @@ "open": "Actions on open" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to set the state of the lock. The lock is locked if the template evaluates to `True`, `true`, `on`, or `locked`. The lock is unlocked if the template evaluates to `False`, `false`, `off`, or `unlocked`. Other valid states are `jammed`, `opening`, `locking`, `open`, and `unlocking`.", + "lock": "Defines actions to run when the lock is locked.", + "unlock": "Defines actions to run when the lock is unlocked.", + "code_format": "Defines a template to get the `code_format` attribute of the lock.", + "open": "Defines actions to run when the lock is opened." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -223,13 +276,22 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Template for the number's current value.", + "step": "Template for the number's increment/decrement step.", + "set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.", + "max": "Template for the number's maximum value.", + "min": "Template for the number's minimum value.", + "unit_of_measurement": "Defines the units of measurement of the number, if any." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -244,13 +306,19 @@ "options": "Available options" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Template for the select’s current value.", + "select_option": "Defines actions to run when an `option` from the `options` list is selected. Receives variable `option`.", + "options": "Template for the select’s available options." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -266,13 +334,18 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the sensor. If the sensor is numeric, i.e. it has a `state_class` or a `unit_of_measurement`, the state template must render to a number or to `none`. The state template must not render to a string, including `unknown` or `unavailable`. An `availability` template may be defined to suppress rendering of the state template.", + "unit_of_measurement": "Defines the units of measurement of the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -307,13 +380,18 @@ }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", - "value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful." + "value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful.", + "turn_off": "Defines actions to run when the switch is turned off.", + "turn_on": "Defines actions to run when the switch is turned on." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -324,24 +402,37 @@ "device_id": "[%key:common::config_flow::data::device%]", "name": "[%key:common::config_flow::data::name%]", "state": "[%key:component::template::common::state%]", - "start": "Actions on turn off", + "start": "Actions on start", "fan_speed": "Fan speed", "fan_speeds": "Fan speeds", "set_fan_speed": "Actions on set fan speed", "stop": "Actions on stop", "pause": "Actions on pause", - "return_to_base": "Actions on return to base", + "return_to_base": "Actions on return to dock", "clean_spot": "Actions on clean spot", "locate": "Actions on locate" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the vacuum. Valid values are `cleaning`, `docked`, `idle`, `paused`, `returning`, and `error`.", + "start": "Defines actions to run when the vacuum is started.", + "fan_speed": "Defines a template to get the fan speed of the vacuum.", + "fan_speeds": "List of fan speeds supported by the vacuum.", + "set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`", + "stop": "Defines actions to run when the vacuum is stopped.", + "pause": "Defines actions to run when the vacuum is paused.", + "return_to_base": "Defines actions to run when the vacuum is given a 'Return to dock' command.", + "clean_spot": "Defines actions to run when the vacuum is given a 'Clean spot' command.", + "locate": "Defines actions to run when the vacuum is given a 'Locate' command." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -366,13 +457,26 @@ "code_format": "[%key:component::template::common::code_format%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "value_template": "[%key:component::template::config::step::alarm_control_panel::data_description::value_template%]", + "disarm": "[%key:component::template::config::step::alarm_control_panel::data_description::disarm%]", + "arm_away": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_away%]", + "arm_custom_bypass": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_custom_bypass%]", + "arm_home": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_home%]", + "arm_night": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_night%]", + "arm_vacation": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_vacation%]", + "trigger": "[%key:component::template::config::step::alarm_control_panel::data_description::trigger%]", + "code_arm_required": "[%key:component::template::config::step::alarm_control_panel::data_description::code_arm_required%]", + "code_format": "[%key:component::template::config::step::alarm_control_panel::data_description::code_format%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -384,13 +488,17 @@ "state": "[%key:component::template::common::state%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::binary_sensor::data_description::state%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -402,13 +510,17 @@ "press": "[%key:component::template::config::step::button::data::press%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "press": "[%key:component::template::config::step::button::data_description::press%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -439,6 +551,9 @@ "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -468,6 +583,9 @@ "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -480,13 +598,18 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "url": "[%key:component::template::config::step::image::data_description::url%]", + "verify_ssl": "[%key:component::template::config::step::image::data_description::verify_ssl%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -507,13 +630,25 @@ "set_temperature": "[%key:component::template::config::step::light::data::set_temperature%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::light::data_description::state%]", + "turn_off": "[%key:component::template::config::step::light::data_description::turn_off%]", + "turn_on": "[%key:component::template::config::step::light::data_description::turn_on%]", + "level": "[%key:component::template::config::step::light::data_description::level%]", + "set_level": "[%key:component::template::config::step::light::data_description::set_level%]", + "hs": "[%key:component::template::config::step::light::data_description::hs%]", + "set_hs": "[%key:component::template::config::step::light::data_description::set_hs%]", + "temperature": "[%key:component::template::config::step::light::data_description::temperature%]", + "set_temperature": "[%key:component::template::config::step::light::data_description::set_temperature%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -529,13 +664,21 @@ "open": "[%key:component::template::config::step::lock::data::open%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::lock::data_description::state%]", + "lock": "[%key:component::template::config::step::lock::data_description::lock%]", + "unlock": "[%key:component::template::config::step::lock::data_description::unlock%]", + "code_format": "[%key:component::template::config::step::lock::data_description::code_format%]", + "open": "[%key:component::template::config::step::lock::data_description::open%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -552,13 +695,21 @@ "min": "[%key:component::template::config::step::number::data::min%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::number::data_description::state%]", + "step": "[%key:component::template::config::step::number::data_description::step%]", + "set_value": "[%key:component::template::config::step::number::data_description::set_value%]", + "max": "[%key:component::template::config::step::number::data_description::max%]", + "min": "[%key:component::template::config::step::number::data_description::min%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -573,13 +724,19 @@ "options": "[%key:component::template::config::step::select::data::options%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::select::data_description::state%]", + "select_option": "[%key:component::template::config::step::select::data_description::select_option%]", + "options": "[%key:component::template::config::step::select::data_description::options%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -594,13 +751,18 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::sensor::data_description::state%]", + "unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::state%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -616,13 +778,18 @@ }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", - "value_template": "[%key:component::template::config::step::switch::data_description::value_template%]" + "value_template": "[%key:component::template::config::step::switch::data_description::value_template%]", + "turn_off": "[%key:component::template::config::step::switch::data_description::turn_off%]", + "turn_on": "[%key:component::template::config::step::switch::data_description::turn_on%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -644,17 +811,30 @@ "locate": "[%key:component::template::config::step::vacuum::data::locate%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::vacuum::data_description::state%]", + "start": "[%key:component::template::config::step::vacuum::data_description::start%]", + "fan_speed": "[%key:component::template::config::step::vacuum::data_description::fan_speed%]", + "fan_speeds": "[%key:component::template::config::step::vacuum::data_description::fan_speeds%]", + "set_fan_speed": "[%key:component::template::config::step::vacuum::data_description::set_fan_speed%]", + "stop": "[%key:component::template::config::step::vacuum::data_description::stop%]", + "pause": "[%key:component::template::config::step::vacuum::data_description::pause%]", + "return_to_base": "[%key:component::template::config::step::vacuum::data_description::return_to_base%]", + "clean_spot": "[%key:component::template::config::step::vacuum::data_description::clean_spot%]", + "locate": "[%key:component::template::config::step::vacuum::data_description::locate%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, - "title": "Template vacuum" + "title": "[%key:component::template::config::step::vacuum::title%]" } } }, From ec7fb140ac43bf54f69ba83fbac39fb61df89405 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Wed, 30 Jul 2025 23:38:11 +0200 Subject: [PATCH 150/265] Fix Miele induction hob empty state (#149706) --- homeassistant/components/miele/sensor.py | 2 +- .../miele/snapshots/test_sensor.ambr | 1137 +++++++++++++++++ tests/components/miele/test_sensor.py | 15 + 3 files changed, 1153 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 216b91ca68e..cc108841aae 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -731,7 +731,7 @@ class MielePlateSensor(MieleSensor): ) ).name if self.device.state_plate_step - else PlatePowerStep.plate_step_0 + else PlatePowerStep.plate_step_0.name ) diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 915eda4d361..2805a683077 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1,4 +1,1141 @@ # serializer version: 1 +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:pot-steam-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_74-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction', + 'icon': 'mdi:pot-steam-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'in_use', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:pot-steam-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_74_off-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction', + 'icon': 'mdi:pot-steam-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 1', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 1', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_1_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 1', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 1', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_1_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 2', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 2', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_3', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_2_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 2', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 2', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_2_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 3', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_7', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_3_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 3', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_3_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 4', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 4', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_15', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_4_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 4', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 4', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_4_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 5', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-5', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 5', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_boost', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hood-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hood', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:turbine', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_18-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hood-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hood', + 'icon': 'mdi:turbine', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hood', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index f35404a665b..e5051a683c9 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -256,3 +256,18 @@ async def test_vacuum_sensor_states( """Test robot vacuum cleaner sensor state.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("load_device_file", ["fan_devices.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_fan_hob_sensor_states( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: None, +) -> None: + """Test robot fan / hob sensor state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) From f318766021f49238c9511ec27f5e4a923105acca Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 30 Jul 2025 23:42:53 +0200 Subject: [PATCH 151/265] Fix inconsistent use of the term 'target' and a typo in MQTT translation strings (#149703) --- homeassistant/components/mqtt/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index c14bda008d1..40215b0f2c6 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -802,15 +802,15 @@ "data": { "max_humidity": "Maximum humidity", "min_humidity": "Minimum humidity", - "target_humidity_command_template": "Humidity command template", - "target_humidity_command_topic": "Humidity command topic", - "target_humidity_state_template": "Humidity state template", - "target_humidity_state_topic": "Humidity state topic" + "target_humidity_command_template": "Target humidity command template", + "target_humidity_command_topic": "Target humidity command topic", + "target_humidity_state_template": "Target humidity state template", + "target_humidity_state_topic": "Target humidity state topic" }, "data_description": { "max_humidity": "The maximum target humidity that can be set.", "min_humidity": "The minimum target humidity that can be set.", - "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the humidity command topic.", + "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the target humidity command topic.", "target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)", "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.", "target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)" @@ -838,7 +838,7 @@ "temperature_low_state_topic": "Lower temperature state topic" }, "data_description": { - "initial": "The climate initalizes with this target temperature.", + "initial": "The climate initializes with this target temperature.", "max_temp": "The maximum target temperature that can be set.", "min_temp": "The minimum target temperature that can be set.", "precision": "The precision in degrees the thermostat is working at.", From 2cf144fb254b7950c27ea399cc1015fcb5c0113e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 30 Jul 2025 23:45:05 +0200 Subject: [PATCH 152/265] Add missing translations for miele dishwasher (#149702) --- homeassistant/components/miele/const.py | 10 ++++++++++ homeassistant/components/miele/strings.json | 3 +++ 2 files changed, 13 insertions(+) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index a40df909e14..e8b626af785 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -431,6 +431,16 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = { 38: "quick_power_wash", 42: "tall_items", 44: "power_wash", + 200: "eco", + 202: "automatic", + 203: "comfort_wash", + 204: "power_wash", + 205: "intensive", + 207: "extra_quiet", + 209: "comfort_wash_plus", + 210: "gentle", + 214: "maintenance", + 215: "rinse_salt", } TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = { -1: "no_program", # Extrapolated from other device types. diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 01f13c8550d..a4400ff26eb 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -485,6 +485,8 @@ "cook_bacon": "Cook bacon", "biscuits_short_crust_pastry_1_tray": "Biscuits, short crust pastry (1 tray)", "biscuits_short_crust_pastry_2_trays": "Biscuits, short crust pastry (2 trays)", + "comfort_wash": "Comfort wash", + "comfort_wash_plus": "Comfort wash plus", "cool_air": "Cool air", "corn_on_the_cob": "Corn on the cob", "cottons": "Cottons", @@ -827,6 +829,7 @@ "rice_pudding_steam_cooking": "Rice pudding (steam cooking)", "rinse": "Rinse", "rinse_out_lint": "Rinse out lint", + "rinse_salt": "Rinse salt", "risotto": "Risotto", "ristretto": "Ristretto", "roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)", From 94dc2e2ea302a7e6ef64319c8f5d37987ec5c35f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 30 Jul 2025 23:54:32 +0200 Subject: [PATCH 153/265] Bump reolink-aio to 0.14.5 (#149700) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 39541476429..efd9f1121b6 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.14.4"] + "requirements": ["reolink-aio==0.14.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index f5f0c5116dc..23ff02d69c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2666,7 +2666,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.4 +reolink-aio==0.14.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9336bbcc68c..9ede8c8f89b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2212,7 +2212,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.4 +reolink-aio==0.14.5 # homeassistant.components.rflink rflink==0.0.67 From f9e7459901c14b9015ac16fcb0746d941c9612de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Jul 2025 13:06:08 -1000 Subject: [PATCH 154/265] Fix ESPHome unnecessary probing on DHCP discovery (#149713) --- .../components/esphome/config_flow.py | 7 ++-- tests/components/esphome/test_config_flow.py | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index dc0e9b8e1b1..4efb0e494ef 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -316,10 +316,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # Don't call _fetch_device_info() for ignored entries raise AbortFlow("already_configured") configured_host: str | None = entry.data.get(CONF_HOST) - configured_port: int | None = entry.data.get(CONF_PORT) - if configured_host == host and configured_port == port: + configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT) + # When port is None (from DHCP discovery), only compare hosts + if configured_host == host and (port is None or configured_port == port): # Don't probe to verify the mac is correct since - # the host and port matches. + # the host matches (and port matches if provided). raise AbortFlow("already_configured") configured_psk: str | None = entry.data.get(CONF_NOISE_PSK) await self._fetch_device_info(host, port or configured_port, configured_psk) diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index d76991a984c..0fda7714dd0 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -2485,3 +2485,36 @@ async def test_reconfig_name_conflict_overwrite( ) is None ) + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_discovery_dhcp_no_probe_same_host_port_none( + hass: HomeAssistant, mock_client: APIClient +) -> None: + """Test dhcp discovery does not probe when host matches and port is None.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + # DHCP discovery with same MAC and host (WiFi device) + service_info = DhcpServiceInfo( + ip="192.168.43.183", + hostname="test8266", + macaddress="11:22:33:44:55:aa", # Same MAC as configured + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Verify device_info was NOT called (no probing) + mock_client.device_info.assert_not_called() + + # Host should remain unchanged + assert entry.data[CONF_HOST] == "192.168.43.183" From 7a55373b0b4a58da107f7905fcb1aadf26fb0c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 31 Jul 2025 01:07:12 +0200 Subject: [PATCH 155/265] Fix bug when interpreting miele action response (#149710) --- homeassistant/components/miele/services.py | 2 +- tests/components/miele/fixtures/programs.json | 4 ++++ tests/components/miele/snapshots/test_services.ambr | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/miele/services.py b/homeassistant/components/miele/services.py index 9854196ea65..517b489173d 100644 --- a/homeassistant/components/miele/services.py +++ b/homeassistant/components/miele/services.py @@ -203,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse: else {} ), } - if item["parameters"] + if item.get("parameters") else {} ), } diff --git a/tests/components/miele/fixtures/programs.json b/tests/components/miele/fixtures/programs.json index ce2348f61de..1c232059d59 100644 --- a/tests/components/miele/fixtures/programs.json +++ b/tests/components/miele/fixtures/programs.json @@ -30,5 +30,9 @@ "mandatory": true } } + }, + { + "programId": 24000, + "program": "Ristretto" } ] diff --git a/tests/components/miele/snapshots/test_services.ambr b/tests/components/miele/snapshots/test_services.ambr index 3095ec9b6fb..3c3feca7832 100644 --- a/tests/components/miele/snapshots/test_services.ambr +++ b/tests/components/miele/snapshots/test_services.ambr @@ -43,6 +43,12 @@ 'program': 'Fan plus', 'program_id': 13, }), + dict({ + 'parameters': dict({ + }), + 'program': 'Ristretto', + 'program_id': 24000, + }), ]), }) # --- From 63216b77c27f8755df393ae2a70c473929305959 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Jul 2025 13:54:18 -1000 Subject: [PATCH 156/265] Bump aioesphomeapi to 37.1.6 (#149715) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 00d56955aa7..355089555c5 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.1.5", + "aioesphomeapi==37.1.6", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 23ff02d69c2..2af9a9f712d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.5 +aioesphomeapi==37.1.6 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ede8c8f89b..bbfcb8b2435 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.5 +aioesphomeapi==37.1.6 # homeassistant.components.flo aioflo==2021.11.0 From ad0db5c83afd0bda54d6f0277fdab79654611a84 Mon Sep 17 00:00:00 2001 From: johanzander Date: Thu, 31 Jul 2025 08:17:33 +0200 Subject: [PATCH 157/265] Update growattServer to version 1.7.1 (#149716) --- homeassistant/components/growatt_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 7b3e67228b1..b6a730835bb 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/growatt_server", "iot_class": "cloud_polling", "loggers": ["growattServer"], - "requirements": ["growattServer==1.6.0"] + "requirements": ["growattServer==1.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2af9a9f712d..3c3e421f5e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1100,7 +1100,7 @@ greenwavereality==0.5.1 gridnet==5.0.1 # homeassistant.components.growatt_server -growattServer==1.6.0 +growattServer==1.7.1 # homeassistant.components.google_sheets gspread==5.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbfcb8b2435..9a0a86ca37f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -961,7 +961,7 @@ greeneye_monitor==3.0.3 gridnet==5.0.1 # homeassistant.components.growatt_server -growattServer==1.6.0 +growattServer==1.7.1 # homeassistant.components.google_sheets gspread==5.5.0 From f7eacaa48d504db58bb423e19a1e904bbaac2a26 Mon Sep 17 00:00:00 2001 From: "L." Date: Thu, 31 Jul 2025 09:01:06 +0200 Subject: [PATCH 158/265] Bump xiaomi-ble to 1.2.0 (#149711) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 2897fbbdb16..bd318c5e30b 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==1.1.0"] + "requirements": ["xiaomi-ble==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3c3e421f5e9..1e20bfcd6c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3139,7 +3139,7 @@ wyoming==1.7.1 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==1.1.0 +xiaomi-ble==1.2.0 # homeassistant.components.knx xknx==3.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a0a86ca37f..b4d412335a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2592,7 +2592,7 @@ wyoming==1.7.1 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==1.1.0 +xiaomi-ble==1.2.0 # homeassistant.components.knx xknx==3.8.0 From 42101dd432cac23c67c68ba98e051c3b137934be Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Jul 2025 10:58:36 +0200 Subject: [PATCH 159/265] Remove result from FlowResult (#149202) --- homeassistant/auth/models.py | 5 ++++- homeassistant/components/auth/login_flow.py | 5 +++-- homeassistant/components/config/config_entries.py | 5 +++-- homeassistant/components/repairs/issue_handler.py | 2 -- homeassistant/config_entries.py | 4 ++-- homeassistant/data_entry_flow.py | 1 - homeassistant/helpers/data_entry_flow.py | 2 +- tests/components/cloud/test_repairs.py | 1 - .../generic_hygrostat/snapshots/test_config_flow.ambr | 1 - .../generic_thermostat/snapshots/test_config_flow.ambr | 1 - tests/components/hassio/test_repairs.py | 1 - .../components/homeassistant_sky_connect/test_config_flow.py | 1 - tests/components/homeassistant_yellow/test_config_flow.py | 1 - tests/components/jewish_calendar/test_config_flow.py | 1 - tests/components/repairs/test_websocket_api.py | 1 - 15 files changed, 13 insertions(+), 19 deletions(-) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 7dcccbb1a1e..f92ed38ad85 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -33,7 +33,10 @@ class AuthFlowContext(FlowContext, total=False): redirect_uri: str -AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]] +class AuthFlowResult(FlowResult[AuthFlowContext, tuple[str, str]], total=False): + """Typed result dict for auth flow.""" + + result: Credentials # Only present if type is CREATE_ENTRY @attr.s(slots=True) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index d27235123b9..fe7ccededf2 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -268,7 +268,7 @@ class LoginFlowBaseView(HomeAssistantView): result.pop("data") result.pop("context") - result_obj: Credentials = result.pop("result") + result_obj = result.pop("result") # Result can be None if credential was never linked to a user before. user = await hass.auth.async_get_user_by_credentials(result_obj) @@ -281,7 +281,8 @@ class LoginFlowBaseView(HomeAssistantView): ) process_success_login(request) - result["result"] = self._store_result(client_id, result_obj) + # We overwrite the Credentials object with the string code to retrieve it. + result["result"] = self._store_result(client_id, result_obj) # type: ignore[typeddict-item] return self.json(result) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index d20d4de881f..a9aafcfaa5e 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -146,8 +146,9 @@ def _prepare_config_flow_result_json( return prepare_result_json(result) data = result.copy() - entry: config_entries.ConfigEntry = data["result"] - data["result"] = entry.as_json_fragment + entry: config_entries.ConfigEntry = data["result"] # type: ignore[typeddict-item] + # We overwrite the ConfigEntry object with its json representation. + data["result"] = entry.as_json_fragment # type: ignore[typeddict-unknown-key] data.pop("data") data.pop("context") return data diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index cc7e017699d..63da15b1ede 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -89,8 +89,6 @@ class RepairsFlowManager(data_entry_flow.FlowManager): """ if result.get("type") != data_entry_flow.FlowResultType.ABORT: ir.async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"]) - if "result" not in result: - result["result"] = None return result diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1c4f2b51ac7..da8e73d9566 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -298,8 +298,10 @@ class ConfigFlowContext(FlowContext, total=False): class ConfigFlowResult(FlowResult[ConfigFlowContext, str], total=False): """Typed result dict for config flow.""" + # Extra keys, only present if type is CREATE_ENTRY minor_version: int options: Mapping[str, Any] + result: ConfigEntry subentries: Iterable[ConfigSubentryData] version: int @@ -3345,7 +3347,6 @@ class ConfigSubentryFlowManager( ), ) - result["result"] = True return result @@ -3508,7 +3509,6 @@ class OptionsFlowManager( ): self.hass.config_entries.async_schedule_reload(entry.entry_id) - result["result"] = True return result async def _async_setup_preview( diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index ce1c0806b14..6b2f9a4dc5c 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -142,7 +142,6 @@ class FlowResult(TypedDict, Generic[_FlowContextT, _HandlerT], total=False): progress_task: asyncio.Task[Any] | None reason: str required: bool - result: Any step_id: str title: str translation_domain: str diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 65eb2786aaf..22074fb90a7 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -35,7 +35,7 @@ class _BaseFlowManagerView(HomeAssistantView, Generic[_FlowManagerT]): """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() - data.pop("result") + assert "result" not in result data.pop("data") data.pop("context") return data diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index d131d211e2f..bb3c874c077 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -236,7 +236,6 @@ async def test_legacy_subscription_repair_flow_timeout( "handler": "cloud", "reason": "operation_took_too_long", "description_placeholders": None, - "result": None, } assert issue_registry.async_get_issue( diff --git a/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr b/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr index 3527596c9b9..859c0eeb1fe 100644 --- a/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr +++ b/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr @@ -15,7 +15,6 @@ # --- # name: test_options[create_entry] FlowResultSnapshot({ - 'result': True, 'type': , }) # --- diff --git a/tests/components/generic_thermostat/snapshots/test_config_flow.ambr b/tests/components/generic_thermostat/snapshots/test_config_flow.ambr index ed757d1c2ae..e69e51e19cd 100644 --- a/tests/components/generic_thermostat/snapshots/test_config_flow.ambr +++ b/tests/components/generic_thermostat/snapshots/test_config_flow.ambr @@ -39,7 +39,6 @@ # --- # name: test_options[create_entry] FlowResultSnapshot({ - 'result': True, 'type': , }) # --- diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 4c4f0e24dcc..39f9d4580bd 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -471,7 +471,6 @@ async def test_mount_failed_repair_flow_error( "flow_id": flow_id, "handler": "hassio", "reason": "apply_suggestion_fail", - "result": None, "description_placeholders": None, } diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 4df3efab360..bdde5e09ea6 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -211,7 +211,6 @@ async def test_options_flow( ) assert create_result["type"] is FlowResultType.CREATE_ENTRY - assert create_result["result"] is True assert config_entry.data == { "firmware": "ezsp", diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index d5f1c380971..6e2120aa961 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -406,7 +406,6 @@ async def test_firmware_options_flow( ) assert create_result["type"] is FlowResultType.CREATE_ENTRY - assert create_result["result"] is True assert config_entry.data == { "firmware": fw_type.value, diff --git a/tests/components/jewish_calendar/test_config_flow.py b/tests/components/jewish_calendar/test_config_flow.py index a63d9abb9a7..234cae2adca 100644 --- a/tests/components/jewish_calendar/test_config_flow.py +++ b/tests/components/jewish_calendar/test_config_flow.py @@ -110,7 +110,6 @@ async def test_options_reconfigure( CONF_CANDLE_LIGHT_MINUTES: DEFAULT_CANDLE_LIGHT + 1, }, ) - assert result["result"] # The value of the "upcoming_shabbat_candle_lighting" sensor should be the new value assert config_entry.options[CONF_CANDLE_LIGHT_MINUTES] == DEFAULT_CANDLE_LIGHT + 1 diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index bbaf70e0a9b..1474e90c8ea 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -599,7 +599,6 @@ async def test_fix_issue_aborted( "handler": "fake_integration", "reason": "not_given", "description_placeholders": None, - "result": None, } await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) From 3952544822a3f4d77cd4f66fd022267d830d5640 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:06:04 +0200 Subject: [PATCH 160/265] Fix ContextVar deprecation warning in homeassistant_hardware integration (#149687) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com> Co-authored-by: mib1185 <35783820+mib1185@users.noreply.github.com> --- .../components/homeassistant_hardware/coordinator.py | 10 +++++++++- .../components/homeassistant_sky_connect/update.py | 1 + .../components/homeassistant_yellow/update.py | 1 + .../homeassistant_hardware/test_coordinator.py | 6 +++++- tests/components/homeassistant_hardware/test_update.py | 2 ++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/coordinator.py b/homeassistant/components/homeassistant_hardware/coordinator.py index c9a5c891328..36a2f407282 100644 --- a/homeassistant/components/homeassistant_hardware/coordinator.py +++ b/homeassistant/components/homeassistant_hardware/coordinator.py @@ -12,6 +12,7 @@ from ha_silabs_firmware_client import ( ManifestMissing, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,13 +25,20 @@ FIRMWARE_REFRESH_INTERVAL = timedelta(hours=8) class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]): """Coordinator to manage firmware updates.""" - def __init__(self, hass: HomeAssistant, session: ClientSession, url: str) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + session: ClientSession, + url: str, + ) -> None: """Initialize the firmware update coordinator.""" super().__init__( hass, _LOGGER, name="firmware update coordinator", update_interval=FIRMWARE_REFRESH_INTERVAL, + config_entry=config_entry, ) self.hass = hass self.session = session diff --git a/homeassistant/components/homeassistant_sky_connect/update.py b/homeassistant/components/homeassistant_sky_connect/update.py index 74c28b37eaf..df69b6d40a2 100644 --- a/homeassistant/components/homeassistant_sky_connect/update.py +++ b/homeassistant/components/homeassistant_sky_connect/update.py @@ -124,6 +124,7 @@ def _async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, NABU_CASA_FIRMWARE_RELEASES_URL, ), diff --git a/homeassistant/components/homeassistant_yellow/update.py b/homeassistant/components/homeassistant_yellow/update.py index 9531bd456cb..7a6e2f19b1f 100644 --- a/homeassistant/components/homeassistant_yellow/update.py +++ b/homeassistant/components/homeassistant_yellow/update.py @@ -129,6 +129,7 @@ def _async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, NABU_CASA_FIRMWARE_RELEASES_URL, ), diff --git a/tests/components/homeassistant_hardware/test_coordinator.py b/tests/components/homeassistant_hardware/test_coordinator.py index 9c57aac6811..39fef3366ad 100644 --- a/tests/components/homeassistant_hardware/test_coordinator.py +++ b/tests/components/homeassistant_hardware/test_coordinator.py @@ -13,6 +13,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry + async def test_firmware_update_coordinator_fetching( hass: HomeAssistant, caplog: pytest.LogCaptureFixture @@ -20,6 +22,8 @@ async def test_firmware_update_coordinator_fetching( """Test the firmware update coordinator loads manifests.""" session = async_get_clientsession(hass) + mock_config_entry = MockConfigEntry() + manifest = FirmwareManifest( url=URL("https://example.org/firmware"), html_url=URL("https://example.org/release_notes"), @@ -35,7 +39,7 @@ async def test_firmware_update_coordinator_fetching( return_value=mock_client, ): coordinator = FirmwareUpdateCoordinator( - hass, session, "https://example.org/firmware" + hass, mock_config_entry, session, "https://example.org/firmware" ) listener = Mock() diff --git a/tests/components/homeassistant_hardware/test_update.py b/tests/components/homeassistant_hardware/test_update.py index aacc064e4f2..3103e5cfc6a 100644 --- a/tests/components/homeassistant_hardware/test_update.py +++ b/tests/components/homeassistant_hardware/test_update.py @@ -143,6 +143,7 @@ def _mock_async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, TEST_FIRMWARE_RELEASES_URL, ), @@ -593,6 +594,7 @@ async def test_update_entity_graceful_firmware_type_callback_errors( config_entry=update_config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + update_config_entry, session, TEST_FIRMWARE_RELEASES_URL, ), From f7c8cdb3a78b2484efb495df1a445617476bc3c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 00:10:23 -1000 Subject: [PATCH 161/265] Bump aioesphomeapi to 37.2.0 (#149732) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 355089555c5..5a7c9a5f927 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.1.6", + "aioesphomeapi==37.2.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 1e20bfcd6c2..49a50982f32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.6 +aioesphomeapi==37.2.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4d412335a6..8b992f2630c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.6 +aioesphomeapi==37.2.0 # homeassistant.components.flo aioflo==2021.11.0 From 3d744f032fb1b9042f027d3fd544b64e543a2354 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Jul 2025 12:35:13 +0200 Subject: [PATCH 162/265] Make _EventDeviceRegistryUpdatedData_Remove JSON serializable (#149734) --- homeassistant/helpers/device_registry.py | 4 ++-- homeassistant/helpers/entity_registry.py | 6 +++--- tests/helpers/test_device_registry.py | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index bc6e7c810bf..c8b4428a7cc 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -156,7 +156,7 @@ class _EventDeviceRegistryUpdatedData_Remove(TypedDict): action: Literal["remove"] device_id: str - device: DeviceEntry + device: dict[str, Any] class _EventDeviceRegistryUpdatedData_Update(TypedDict): @@ -1319,7 +1319,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): self.hass.bus.async_fire_internal( EVENT_DEVICE_REGISTRY_UPDATED, _EventDeviceRegistryUpdatedData_Remove( - action="remove", device_id=device_id, device=device + action="remove", device_id=device_id, device=device.dict_repr ), ) self.async_schedule_save() diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7051521b805..d972b421fc4 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1103,13 +1103,13 @@ class EntityRegistry(BaseRegistry): entities = async_entries_for_device( self, event.data["device_id"], include_disabled_entities=True ) - removed_device = event.data["device"] + removed_device_dict = event.data["device"] for entity in entities: config_entry_id = entity.config_entry_id if ( - config_entry_id in removed_device.config_entries + config_entry_id in removed_device_dict["config_entries"] and entity.config_subentry_id - in removed_device.config_entries_subentries[config_entry_id] + in removed_device_dict["config_entries_subentries"][config_entry_id] ): self.async_remove(entity.entity_id) else: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 23a451dd06c..a66684c94e3 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1652,7 +1652,7 @@ async def test_removing_config_entries( assert update_events[4].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } @@ -1725,12 +1725,12 @@ async def test_deleted_device_removing_config_entries( assert update_events[3].data == { "action": "remove", "device_id": entry.id, - "device": entry2, + "device": entry2.dict_repr, } assert update_events[4].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } device_registry.async_clear_config_entry(config_entry_1.entry_id) @@ -1976,7 +1976,7 @@ async def test_removing_config_subentries( assert update_events[7].data == { "action": "remove", "device_id": entry.id, - "device": entry, + "device": entry.dict_repr, } @@ -2106,7 +2106,7 @@ async def test_deleted_device_removing_config_subentries( assert update_events[4].data == { "action": "remove", "device_id": entry.id, - "device": entry4, + "device": entry4.dict_repr, } device_registry.async_clear_config_subentry(config_entry_1.entry_id, None) @@ -2930,7 +2930,7 @@ async def test_update_remove_config_entries( assert update_events[6].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } @@ -3208,7 +3208,7 @@ async def test_update_remove_config_subentries( assert update_events[7].data == { "action": "remove", "device_id": entry_id, - "device": entry_before_remove, + "device": entry_before_remove.dict_repr, } @@ -3551,7 +3551,7 @@ async def test_restore_device( assert update_events[2].data == { "action": "remove", "device_id": entry.id, - "device": entry, + "device": entry.dict_repr, } assert update_events[3].data == { "action": "create", @@ -3874,7 +3874,7 @@ async def test_restore_shared_device( assert update_events[3].data == { "action": "remove", "device_id": entry.id, - "device": updated_device, + "device": updated_device.dict_repr, } assert update_events[4].data == { "action": "create", @@ -3883,7 +3883,7 @@ async def test_restore_shared_device( assert update_events[5].data == { "action": "remove", "device_id": entry.id, - "device": entry2, + "device": entry2.dict_repr, } assert update_events[6].data == { "action": "create", From 04fb86b4ba027ce68ccdd3d079151744c0a71057 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:19:37 -0400 Subject: [PATCH 163/265] Fix unique_id in config validation for legacy weather platform (#149742) --- homeassistant/components/template/weather.py | 2 ++ tests/components/template/test_weather.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 7f79adc2201..bddb55197c3 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -34,6 +34,7 @@ from homeassistant.components.weather import ( from homeassistant.const import ( CONF_NAME, CONF_TEMPERATURE_UNIT, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -151,6 +152,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS), vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS), vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template, diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index 6e2a2ab2f6b..7eac7ff28aa 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -132,6 +132,7 @@ async def setup_weather( { "platform": "template", "name": "test", + "unique_id": "abc123", "attribution_template": "{{ states('sensor.attribution') }}", "condition_template": "sunny", "temperature_template": "{{ states('sensor.temperature') | float }}", From 59d8df142db4142d3e2407018b970ef57defeffc Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:19:43 -0400 Subject: [PATCH 164/265] Nitpick default translations for template integration (#149740) --- homeassistant/components/template/strings.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index b412fa519cd..d29bfbeb3fb 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -39,7 +39,7 @@ "arm_vacation": "Defines actions to run when the alarm control panel is armed to `arm_vacation`. Receives variable `code`.", "trigger": "Defines actions to run when the alarm control panel is triggered. Receives variable `code`.", "code_arm_required": "If true, the code is required to arm the alarm.", - "code_format": "One of number, text or no_code. Format for the code used to arm/disarm the alarm." + "code_format": "One of `number`, `text` or `no_code`. Format for the code used to arm/disarm the alarm." }, "sections": { "advanced_options": { @@ -179,7 +179,7 @@ "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", "url": "Defines a template to get the URL on which the image is served.", - "verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http-only URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification." + "verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification." }, "sections": { "advanced_options": { @@ -282,7 +282,7 @@ "set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.", "max": "Template for the number's maximum value.", "min": "Template for the number's minimum value.", - "unit_of_measurement": "Defines the units of measurement of the number, if any." + "unit_of_measurement": "Defines the unit of measurement of the number, if any." }, "sections": { "advanced_options": { @@ -336,7 +336,7 @@ "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", "state": "Defines a template to get the state of the sensor. If the sensor is numeric, i.e. it has a `state_class` or a `unit_of_measurement`, the state template must render to a number or to `none`. The state template must not render to a string, including `unknown` or `unavailable`. An `availability` template may be defined to suppress rendering of the state template.", - "unit_of_measurement": "Defines the units of measurement of the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value." + "unit_of_measurement": "Defines the unit of measurement for the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value." }, "sections": { "advanced_options": { @@ -418,7 +418,7 @@ "start": "Defines actions to run when the vacuum is started.", "fan_speed": "Defines a template to get the fan speed of the vacuum.", "fan_speeds": "List of fan speeds supported by the vacuum.", - "set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`", + "set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`.", "stop": "Defines actions to run when the vacuum is stopped.", "pause": "Defines actions to run when the vacuum is paused.", "return_to_base": "Defines actions to run when the vacuum is given a 'Return to dock' command.", From 58dc6a952e7958221c08b033d48daf55b048cc29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:35:55 +0200 Subject: [PATCH 165/265] Bump home-assistant/wheels from 2025.03.0 to 2025.07.0 (#149741) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ea02b249dc9..8d9fca093de 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -159,7 +159,7 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@2025.03.0 + uses: home-assistant/wheels@2025.07.0 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -219,7 +219,7 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@2025.03.0 + uses: home-assistant/wheels@2025.07.0 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 From 5f6b1212a31fad66ee4ffd9acc6cb6cc457bd994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 31 Jul 2025 15:04:09 +0100 Subject: [PATCH 166/265] Remove data flow step_id deprecation note (#149714) --- homeassistant/data_entry_flow.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 6b2f9a4dc5c..7408993cc47 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -705,10 +705,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): last_step: bool | None = None, preview: str | None = None, ) -> _FlowResultT: - """Return the definition of a form to gather user input. - - The step_id parameter is deprecated and will be removed in a future release. - """ + """Return the definition of a form to gather user input.""" flow_result = self._flow_result( type=FlowResultType.FORM, flow_id=self.flow_id, @@ -770,10 +767,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): url: str, description_placeholders: Mapping[str, str] | None = None, ) -> _FlowResultT: - """Return the definition of an external step for the user to take. - - The step_id parameter is deprecated and will be removed in a future release. - """ + """Return the definition of an external step for the user to take.""" flow_result = self._flow_result( type=FlowResultType.EXTERNAL_STEP, flow_id=self.flow_id, @@ -804,10 +798,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): description_placeholders: Mapping[str, str] | None = None, progress_task: asyncio.Task[Any] | None = None, ) -> _FlowResultT: - """Show a progress message to the user, without user input allowed. - - The step_id parameter is deprecated and will be removed in a future release. - """ + """Show a progress message to the user, without user input allowed.""" if progress_task is None and not self.__no_progress_task_reported: self.__no_progress_task_reported = True cls = self.__class__ @@ -867,7 +858,6 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): """Show a navigation menu to the user. Options dict maps step_id => i18n label - The step_id parameter is deprecated and will be removed in a future release. """ flow_result = self._flow_result( type=FlowResultType.MENU, From 6ad1b8dcb11b3997e0526a3d1a0b65783843f6c4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Jul 2025 17:49:09 +0200 Subject: [PATCH 167/265] Fix kitchen_sink option flow (#149760) --- homeassistant/components/kitchen_sink/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py index 059fd11999f..056ace7011c 100644 --- a/homeassistant/components/kitchen_sink/config_flow.py +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -99,7 +99,7 @@ class OptionsFlowHandler(OptionsFlowWithReload): ), } ) - self.add_suggested_values_to_schema( + data_schema = self.add_suggested_values_to_schema( data_schema, {"section_1": {"int": self.config_entry.options.get(CONF_INT, 10)}}, ) From f7d54b46ecffcdcbd6b745bba3b932d038b9f88b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Jul 2025 17:55:15 +0200 Subject: [PATCH 168/265] Improve test of FlowHandler.add_suggested_values_to_schema (#149759) --- tests/test_data_entry_flow.py | 42 ++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index a5908f0feab..fc40a330a1a 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -155,19 +155,23 @@ async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) async def async_step_init(self, user_input=None): data_schema = self.add_suggested_values_to_schema( schema, - { - "username": "doej", - "password": "verySecret1", - "section_1": {"full_name": "John Doe"}, - }, + user_input, ) return self.async_show_form( step_id="init", data_schema=data_schema, ) - form = await manager.async_init("test") + form = await manager.async_init( + "test", + data={ + "username": "doej", + "password": "verySecret1", + "section_1": {"full_name": "John Doe"}, + }, + ) assert form["type"] == data_entry_flow.FlowResultType.FORM + assert form["data_schema"].schema is not schema.schema assert form["data_schema"].schema == schema.schema markers = list(form["data_schema"].schema) assert len(markers) == 3 @@ -187,6 +191,32 @@ async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) assert section_markers[0] == "full_name" assert section_markers[0].description == {"suggested_value": "John Doe"} + # Test again without suggested values to make sure we're not mutating the schema + form = await manager.async_init( + "test", + ) + assert form["type"] == data_entry_flow.FlowResultType.FORM + assert form["data_schema"].schema is not schema.schema + assert form["data_schema"].schema == schema.schema + markers = list(form["data_schema"].schema) + assert len(markers) == 3 + assert markers[0] == "username" + assert markers[0].description is None + assert markers[1] == "password" + assert markers[1].description is None + assert markers[2] == "section_1" + section_validator = form["data_schema"].schema["section_1"] + assert isinstance(section_validator, data_entry_flow.section) + # The section class was not replaced + assert section_validator is schema.schema["section_1"] + # The section schema was not replaced + assert section_validator.schema is schema.schema["section_1"].schema + section_markers = list(section_validator.schema.schema) + assert len(section_markers) == 1 + assert section_markers[0] == "full_name" + # This is a known bug, which needs to be fixed + assert section_markers[0].description == {"suggested_value": "John Doe"} + async def test_abort_removes_instance(manager: MockFlowManager) -> None: """Test that abort removes the flow from progress.""" From 21a97990604d703939aff51f928208dbbbabbce1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 31 Jul 2025 18:46:10 +0200 Subject: [PATCH 169/265] Update frontend to 20250731.0 (#149757) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 09461a3543a..706940f5da7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250730.0"] + "requirements": ["home-assistant-frontend==20250731.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 704fb282784..cd0fc31b008 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==4.0.1 hass-nabucasa==0.110.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250730.0 +home-assistant-frontend==20250731.0 home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 49a50982f32..1bbc5a72267 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250730.0 +home-assistant-frontend==20250731.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b992f2630c..51ccb301d1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250730.0 +home-assistant-frontend==20250731.0 # homeassistant.components.conversation home-assistant-intents==2025.7.30 From bbc1466cfc6a899e322ef61a1f62135b4ed3d75b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 31 Jul 2025 18:51:10 +0200 Subject: [PATCH 170/265] Update rpds-py to 0.26.0 (#149753) --- homeassistant/package_constraints.txt | 5 ++--- script/gen_requirements_all.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cd0fc31b008..ac91084c4f1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -209,7 +209,6 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 -# rpds-py > 0.25.0 requires cargo 1.84.0 -# Stable Alpine current only ships cargo 1.83.0 +# rpds-py frequently updates cargo causing build failures # No wheels upstream available for armhf & armv7 -rpds-py==0.24.0 +rpds-py==0.26.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 13bb3384258..b13f586439d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -235,10 +235,9 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 -# rpds-py > 0.25.0 requires cargo 1.84.0 -# Stable Alpine current only ships cargo 1.83.0 +# rpds-py frequently updates cargo causing build failures # No wheels upstream available for armhf & armv7 -rpds-py==0.24.0 +rpds-py==0.26.0 """ GENERATED_MESSAGE = ( From aa6b37bc7c5baa9d8970baeaa5b51f96cd7fa17f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:50:26 -0700 Subject: [PATCH 171/265] Fix `add_suggested_values_to_schema` when the schema has sections (#149718) Co-authored-by: Erik Montnemery --- homeassistant/data_entry_flow.py | 7 ++++--- tests/test_data_entry_flow.py | 32 +++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 7408993cc47..5023d291ad5 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -676,9 +676,10 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): and key in suggested_values ): new_section_key = copy.copy(key) - schema[new_section_key] = val - val.schema = self.add_suggested_values_to_schema( - val.schema, suggested_values[key] + new_val = copy.copy(val) + schema[new_section_key] = new_val + new_val.schema = self.add_suggested_values_to_schema( + new_val.schema, suggested_values[key] ) continue diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index fc40a330a1a..0faa4dd1a80 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -135,6 +135,19 @@ async def test_show_form(manager: MockFlowManager) -> None: async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) -> None: """Test that we can show a form with suggested values.""" + + def compare_schemas(schema: vol.Schema, expected_schema: vol.Schema) -> None: + """Compare two schemas.""" + assert schema.schema is not expected_schema.schema + + assert list(schema.schema) == list(expected_schema.schema) + + for key, validator in schema.schema.items(): + if isinstance(validator, data_entry_flow.section): + assert validator.schema == expected_schema.schema[key].schema + continue + assert validator == expected_schema.schema[key] + schema = vol.Schema( { vol.Required("username"): str, @@ -172,7 +185,8 @@ async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) ) assert form["type"] == data_entry_flow.FlowResultType.FORM assert form["data_schema"].schema is not schema.schema - assert form["data_schema"].schema == schema.schema + assert form["data_schema"].schema != schema.schema + compare_schemas(form["data_schema"], schema) markers = list(form["data_schema"].schema) assert len(markers) == 3 assert markers[0] == "username" @@ -182,10 +196,11 @@ async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) assert markers[2] == "section_1" section_validator = form["data_schema"].schema["section_1"] assert isinstance(section_validator, data_entry_flow.section) - # The section class was not replaced - assert section_validator is schema.schema["section_1"] - # The section schema was not replaced - assert section_validator.schema is schema.schema["section_1"].schema + # The section instance was copied + assert section_validator is not schema.schema["section_1"] + # The section schema instance was copied + assert section_validator.schema is not schema.schema["section_1"].schema + assert section_validator.schema == schema.schema["section_1"].schema section_markers = list(section_validator.schema.schema) assert len(section_markers) == 1 assert section_markers[0] == "full_name" @@ -207,15 +222,14 @@ async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) assert markers[2] == "section_1" section_validator = form["data_schema"].schema["section_1"] assert isinstance(section_validator, data_entry_flow.section) - # The section class was not replaced + # The section class is not replaced if there is no suggested value for the section assert section_validator is schema.schema["section_1"] - # The section schema was not replaced + # The section schema is not replaced if there is no suggested value for the section assert section_validator.schema is schema.schema["section_1"].schema section_markers = list(section_validator.schema.schema) assert len(section_markers) == 1 assert section_markers[0] == "full_name" - # This is a known bug, which needs to be fixed - assert section_markers[0].description == {"suggested_value": "John Doe"} + assert section_markers[0].description is None async def test_abort_removes_instance(manager: MockFlowManager) -> None: From 21c1427abf64d338cf3295ad027b82d6207f2d62 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:52:17 -0400 Subject: [PATCH 172/265] Fix ZHA ContextVar deprecation by passing config_entry (#149748) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com> Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> Co-authored-by: TheJulianJES <6409465+TheJulianJES@users.noreply.github.com> --- homeassistant/components/zha/update.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index 062581fd259..867e4ff2dd3 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -58,7 +58,7 @@ async def async_setup_entry( zha_data = get_zha_data(hass) if zha_data.update_coordinator is None: zha_data.update_coordinator = ZHAFirmwareUpdateCoordinator( - hass, get_zha_gateway(hass).application_controller + hass, config_entry, get_zha_gateway(hass).application_controller ) entities_to_create = zha_data.platforms[Platform.UPDATE] @@ -79,12 +79,16 @@ class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disa """Firmware update coordinator that broadcasts updates network-wide.""" def __init__( - self, hass: HomeAssistant, controller_application: ControllerApplication + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + controller_application: ControllerApplication, ) -> None: """Initialize the coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="ZHA firmware update coordinator", update_method=self.async_update_data, ) From 61ca42e92314d077b699ea02019cc59a563ba412 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 31 Jul 2025 13:04:23 -0600 Subject: [PATCH 173/265] Bump pylitterbot to 2024.2.3 (#149763) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 33addd85ba2..e67c681ac53 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_push", "loggers": ["pylitterbot"], "quality_scale": "bronze", - "requirements": ["pylitterbot==2024.2.2"] + "requirements": ["pylitterbot==2024.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1bbc5a72267..446d7117588 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2122,7 +2122,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.3 # homeassistant.components.litterrobot -pylitterbot==2024.2.2 +pylitterbot==2024.2.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51ccb301d1d..a4bf8743a52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1767,7 +1767,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.3 # homeassistant.components.litterrobot -pylitterbot==2024.2.2 +pylitterbot==2024.2.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.24.0 From 4b5fe424ede41404b17d333e2616e4f9a3004a91 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 1 Aug 2025 00:07:56 +0200 Subject: [PATCH 174/265] Hide configuration URL when Uptime Kuma is installed locally (#149781) --- homeassistant/components/uptime_kuma/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/uptime_kuma/sensor.py b/homeassistant/components/uptime_kuma/sensor.py index c76fbcae04c..b499c67da16 100644 --- a/homeassistant/components/uptime_kuma/sensor.py +++ b/homeassistant/components/uptime_kuma/sensor.py @@ -162,7 +162,11 @@ class UptimeKumaSensorEntity( name=coordinator.data[monitor].monitor_name, identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor!s}")}, manufacturer="Uptime Kuma", - configuration_url=coordinator.config_entry.data[CONF_URL], + configuration_url=( + None + if "127.0.0.1" in (url := coordinator.config_entry.data[CONF_URL]) + else url + ), sw_version=coordinator.api.version.version, ) From eb222f6c5d84d810e8c880b9b89a075e6396bafa Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 1 Aug 2025 00:09:20 +0200 Subject: [PATCH 175/265] Bump motionblinds to 0.6.30 (#149764) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index eca520d8946..ac5390f5c64 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.29"] + "requirements": ["motionblinds==0.6.30"] } diff --git a/requirements_all.txt b/requirements_all.txt index 446d7117588..ef631e829fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1458,7 +1458,7 @@ monzopy==1.5.1 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.29 +motionblinds==0.6.30 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4bf8743a52..e8fc75a6273 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1250,7 +1250,7 @@ monzopy==1.5.1 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.29 +motionblinds==0.6.30 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 From b86b0c10bd92ed4770cc034a68c88a8de4655017 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 12:23:24 -1000 Subject: [PATCH 176/265] Bump aioesphomeapi to 37.2.2 (#149755) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 5a7c9a5f927..6bf164aa9bc 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.2.0", + "aioesphomeapi==37.2.2", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index ef631e829fe..baad7de1409 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.2.0 +aioesphomeapi==37.2.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8fc75a6273..93709fec4b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.2.0 +aioesphomeapi==37.2.2 # homeassistant.components.flo aioflo==2021.11.0 From c72c600de49097e701db879108674e270bb9b7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Lafoucri=C3=A8re?= <12752+gravis@users.noreply.github.com> Date: Thu, 31 Jul 2025 18:47:25 -0400 Subject: [PATCH 177/265] Fix bootstrap script path resolution (#149721) --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index e60342563ac..725cb856bbf 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,7 +4,7 @@ # Stop on errors set -e -cd "$(dirname "$0")/.." +cd "$(realpath "$(dirname "$0")/..")" echo "Installing development dependencies..." uv pip install wheel --constraint homeassistant/package_constraints.txt --upgrade From 61396d92a504eac53e231ca16cbd381a5bcc3378 Mon Sep 17 00:00:00 2001 From: Fabian Leutgeb Date: Fri, 1 Aug 2025 03:21:48 +0200 Subject: [PATCH 178/265] Homekit valve duration characteristics (#149698) Co-authored-by: J. Nick Koston --- .../components/homekit/accessories.py | 4 +- homeassistant/components/homekit/const.py | 4 + .../components/homekit/type_switches.py | 108 ++++++++- homeassistant/components/homekit/util.py | 16 +- .../components/homekit/test_type_switches.py | 226 ++++++++++++++++++ tests/components/homekit/test_util.py | 44 ++++ 6 files changed, 398 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 95842d56094..681ebcbbef7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -628,12 +628,12 @@ class HomeAccessory(Accessory): # type: ignore[misc] self, domain: str, service: str, - service_data: dict[str, Any] | None, + service_data: dict[str, Any], value: Any | None = None, ) -> None: """Fire event and call service for changes from HomeKit.""" event_data = { - ATTR_ENTITY_ID: self.entity_id, + ATTR_ENTITY_ID: service_data.get(ATTR_ENTITY_ID, self.entity_id), ATTR_DISPLAY_NAME: self.display_name, ATTR_SERVICE: service, ATTR_VALUE: value, diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 44f18c30099..2d4e2b03079 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -57,6 +57,8 @@ CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor" CONF_LINKED_OBSTRUCTION_SENSOR = "linked_obstruction_sensor" CONF_LINKED_PM25_SENSOR = "linked_pm25_sensor" CONF_LINKED_TEMPERATURE_SENSOR = "linked_temperature_sensor" +CONF_LINKED_VALVE_DURATION = "linked_valve_duration" +CONF_LINKED_VALVE_END_TIME = "linked_valve_end_time" CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" CONF_MAX_FPS = "max_fps" CONF_MAX_HEIGHT = "max_height" @@ -229,10 +231,12 @@ CHAR_ON = "On" CHAR_OUTLET_IN_USE = "OutletInUse" CHAR_POSITION_STATE = "PositionState" CHAR_PROGRAMMABLE_SWITCH_EVENT = "ProgrammableSwitchEvent" +CHAR_REMAINING_DURATION = "RemainingDuration" CHAR_REMOTE_KEY = "RemoteKey" CHAR_ROTATION_DIRECTION = "RotationDirection" CHAR_ROTATION_SPEED = "RotationSpeed" CHAR_SATURATION = "Saturation" +CHAR_SET_DURATION = "SetDuration" CHAR_SERIAL_NUMBER = "SerialNumber" CHAR_SERVICE_LABEL_INDEX = "ServiceLabelIndex" CHAR_SERVICE_LABEL_NAMESPACE = "ServiceLabelNamespace" diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 18150c820c3..c011b8cd327 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -15,6 +15,11 @@ from pyhap.const import ( ) from homeassistant.components import button, input_button +from homeassistant.components.input_number import ( + ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE, + DOMAIN as INPUT_NUMBER_DOMAIN, + SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE, +) from homeassistant.components.input_select import ATTR_OPTIONS, SERVICE_SELECT_OPTION from homeassistant.components.lawn_mower import ( DOMAIN as LAWN_MOWER_DOMAIN, @@ -45,6 +50,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State, callback, split_entity_id from homeassistant.helpers.event import async_call_later +from homeassistant.util import dt as dt_util from .accessories import TYPES, HomeAccessory, HomeDriver from .const import ( @@ -54,7 +60,11 @@ from .const import ( CHAR_NAME, CHAR_ON, CHAR_OUTLET_IN_USE, + CHAR_REMAINING_DURATION, + CHAR_SET_DURATION, CHAR_VALVE_TYPE, + CONF_LINKED_VALVE_DURATION, + CONF_LINKED_VALVE_END_TIME, SERV_OUTLET, SERV_SWITCH, SERV_VALVE, @@ -271,7 +281,21 @@ class ValveBase(HomeAccessory): self.on_service = on_service self.off_service = off_service - serv_valve = self.add_preload_service(SERV_VALVE) + self.chars = [] + + self.linked_duration_entity: str | None = self.config.get( + CONF_LINKED_VALVE_DURATION + ) + self.linked_end_time_entity: str | None = self.config.get( + CONF_LINKED_VALVE_END_TIME + ) + + if self.linked_duration_entity: + self.chars.append(CHAR_SET_DURATION) + if self.linked_end_time_entity: + self.chars.append(CHAR_REMAINING_DURATION) + + serv_valve = self.add_preload_service(SERV_VALVE, self.chars) self.char_active = serv_valve.configure_char( CHAR_ACTIVE, value=False, setter_callback=self.set_state ) @@ -279,6 +303,25 @@ class ValveBase(HomeAccessory): self.char_valve_type = serv_valve.configure_char( CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type].valve_type ) + + if CHAR_SET_DURATION in self.chars: + _LOGGER.debug( + "%s: Add characteristic %s", self.entity_id, CHAR_SET_DURATION + ) + self.char_set_duration = serv_valve.configure_char( + CHAR_SET_DURATION, + value=self.get_duration(), + setter_callback=self.set_duration, + ) + + if CHAR_REMAINING_DURATION in self.chars: + _LOGGER.debug( + "%s: Add characteristic %s", self.entity_id, CHAR_REMAINING_DURATION + ) + self.char_remaining_duration = serv_valve.configure_char( + CHAR_REMAINING_DURATION, getter_callback=self.get_remaining_duration + ) + # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup self.async_update_state(state) @@ -294,12 +337,75 @@ class ValveBase(HomeAccessory): @callback def async_update_state(self, new_state: State) -> None: """Update switch state after state changed.""" + self._update_duration_chars() current_state = 1 if new_state.state in self.open_states else 0 _LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state) self.char_active.set_value(current_state) _LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state) self.char_in_use.set_value(current_state) + def _update_duration_chars(self) -> None: + """Update valve duration related properties if characteristics are available.""" + if CHAR_SET_DURATION in self.chars: + self.char_set_duration.set_value(self.get_duration()) + if CHAR_REMAINING_DURATION in self.chars: + self.char_remaining_duration.set_value(self.get_remaining_duration()) + + def set_duration(self, value: int) -> None: + """Set default duration for how long the valve should remain open.""" + _LOGGER.debug("%s: Set default run time to %s", self.entity_id, value) + self.async_call_service( + INPUT_NUMBER_DOMAIN, + INPUT_NUMBER_SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: self.linked_duration_entity, + INPUT_NUMBER_ATTR_VALUE: value, + }, + value, + ) + + def get_duration(self) -> int: + """Get the default duration from Home Assistant.""" + duration_state = self._get_entity_state(self.linked_duration_entity) + if duration_state is None: + _LOGGER.debug( + "%s: No linked duration entity state available", self.entity_id + ) + return 0 + + try: + duration = float(duration_state) + return max(int(duration), 0) + except ValueError: + _LOGGER.debug("%s: Cannot parse linked duration entity", self.entity_id) + return 0 + + def get_remaining_duration(self) -> int: + """Calculate the remaining duration based on end time in Home Assistant.""" + end_time_state = self._get_entity_state(self.linked_end_time_entity) + if end_time_state is None: + _LOGGER.debug( + "%s: No linked end time entity state available", self.entity_id + ) + return self.get_duration() + + end_time = dt_util.parse_datetime(end_time_state) + if end_time is None: + _LOGGER.debug("%s: Cannot parse linked end time entity", self.entity_id) + return self.get_duration() + + remaining_time = (end_time - dt_util.utcnow()).total_seconds() + return max(int(remaining_time), 0) + + def _get_entity_state(self, entity_id: str | None) -> str | None: + """Fetch the state of a linked entity.""" + if entity_id is None: + return None + state = self.hass.states.get(entity_id) + if state is None: + return None + return state.state + @TYPES.register("ValveSwitch") class ValveSwitch(ValveBase): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 85207e09626..ea67e30a3c1 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -17,6 +17,7 @@ import voluptuous as vol from homeassistant.components import ( binary_sensor, + input_number, media_player, persistent_notification, sensor, @@ -69,6 +70,8 @@ from .const import ( CONF_LINKED_OBSTRUCTION_SENSOR, CONF_LINKED_PM25_SENSOR, CONF_LINKED_TEMPERATURE_SENSOR, + CONF_LINKED_VALVE_DURATION, + CONF_LINKED_VALVE_END_TIME, CONF_LOW_BATTERY_THRESHOLD, CONF_MAX_FPS, CONF_MAX_HEIGHT, @@ -266,7 +269,9 @@ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend( TYPE_VALVE, ) ), - ) + ), + vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(input_number.DOMAIN), + vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(sensor.DOMAIN), } ) @@ -277,6 +282,12 @@ SENSOR_SCHEMA = BASIC_INFO_SCHEMA.extend( } ) +VALVE_SCHEMA = BASIC_INFO_SCHEMA.extend( + { + vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(input_number.DOMAIN), + vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(sensor.DOMAIN), + } +) HOMEKIT_CHAR_TRANSLATIONS = { 0: " ", # nul @@ -360,6 +371,9 @@ def validate_entity_config(values: dict) -> dict[str, dict]: elif domain == "sensor": config = SENSOR_SCHEMA(config) + elif domain == "valve": + config = VALVE_SCHEMA(config) + else: config = BASIC_INFO_SCHEMA(config) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 3f0f0a3c22b..47a9c398d16 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -2,6 +2,7 @@ from datetime import timedelta +from freezegun import freeze_time import pytest from homeassistant.components.homekit.const import ( @@ -22,6 +23,10 @@ from homeassistant.components.homekit.type_switches import ( Valve, ValveSwitch, ) +from homeassistant.components.input_number import ( + DOMAIN as INPUT_NUMBER_DOMAIN, + SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE, +) from homeassistant.components.lawn_mower import ( DOMAIN as LAWN_MOWER_DOMAIN, SERVICE_DOCK, @@ -30,6 +35,7 @@ from homeassistant.components.lawn_mower import ( LawnMowerEntityFeature, ) from homeassistant.components.select import ATTR_OPTIONS +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, @@ -658,3 +664,223 @@ async def test_button_switch( await hass.async_block_till_done() assert acc.char_on.value is False assert len(events) == 1 + + +async def test_valve_switch_with_set_duration_characteristic( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: + """Test valve switch with set duration characteristic.""" + entity_id = "switch.sprinkler" + + hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set("input_number.valve_duration", "0") + await hass.async_block_till_done() + + # Mock switch services to prevent errors + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_ON) + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_OFF) + + acc = ValveSwitch( + hass, + hk_driver, + "Sprinkler", + entity_id, + 5, + {"type": "sprinkler", "linked_valve_duration": "input_number.valve_duration"}, + ) + acc.run() + await hass.async_block_till_done() + + # Assert initial state is synced + assert acc.get_duration() == 0 + + # Simulate setting duration from HomeKit + call_set_value = async_mock_service( + hass, INPUT_NUMBER_DOMAIN, INPUT_NUMBER_SERVICE_SET_VALUE + ) + acc.char_set_duration.client_update_value(300) + await hass.async_block_till_done() + assert call_set_value + assert call_set_value[0].data == { + "entity_id": "input_number.valve_duration", + "value": 300, + } + + # Assert state change in Home Assistant is synced to HomeKit + hass.states.async_set("input_number.valve_duration", "600") + await hass.async_block_till_done() + assert acc.get_duration() == 600 + + # Test fallback if no state is set + hass.states.async_remove("input_number.valve_duration") + await hass.async_block_till_done() + assert acc.get_duration() == 0 + + # Test remaining duration fallback if no end time is linked + assert acc.get_remaining_duration() == 0 + + +async def test_valve_switch_with_remaining_duration_characteristic( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: + """Test valve switch with remaining duration characteristic.""" + entity_id = "switch.sprinkler" + + hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set("sensor.valve_end_time", dt_util.utcnow().isoformat()) + await hass.async_block_till_done() + + # Mock switch services to prevent errors + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_ON) + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_OFF) + + acc = ValveSwitch( + hass, + hk_driver, + "Sprinkler", + entity_id, + 5, + {"type": "sprinkler", "linked_valve_end_time": "sensor.valve_end_time"}, + ) + acc.run() + await hass.async_block_till_done() + + # Assert initial state is synced + assert acc.get_remaining_duration() == 0 + + # Simulate remaining duration update from Home Assistant + with freeze_time(dt_util.utcnow()): + hass.states.async_set( + "sensor.valve_end_time", + (dt_util.utcnow() + timedelta(seconds=90)).isoformat(), + ) + await hass.async_block_till_done() + + # Assert remaining duration is calculated correctly based on end time + assert acc.get_remaining_duration() == 90 + + # Test fallback if no state is set + hass.states.async_remove("sensor.valve_end_time") + await hass.async_block_till_done() + assert acc.get_remaining_duration() == 0 + + # Test get duration fallback if no duration is linked + assert acc.get_duration() == 0 + + +async def test_valve_switch_with_duration_characteristics( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: + """Test valve switch with set duration and remaining duration characteristics.""" + entity_id = "switch.sprinkler" + + # Test with duration and end time entities linked + hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set("input_number.valve_duration", "300") + hass.states.async_set("sensor.valve_end_time", dt_util.utcnow().isoformat()) + await hass.async_block_till_done() + + # Mock switch services to prevent errors + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_ON) + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_OFF) + # Mock input_number service for set_duration calls + call_set_value = async_mock_service( + hass, INPUT_NUMBER_DOMAIN, INPUT_NUMBER_SERVICE_SET_VALUE + ) + + acc = ValveSwitch( + hass, + hk_driver, + "Sprinkler", + entity_id, + 5, + { + "type": "sprinkler", + "linked_valve_duration": "input_number.valve_duration", + "linked_valve_end_time": "sensor.valve_end_time", + }, + ) + acc.run() + await hass.async_block_till_done() + + # Test update_duration_chars with both characteristics + with freeze_time(dt_util.utcnow()): + hass.states.async_set( + "sensor.valve_end_time", + (dt_util.utcnow() + timedelta(seconds=60)).isoformat(), + ) + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_set_duration.value == 300 + assert acc.get_remaining_duration() == 60 + + # Test get_duration fallback with invalid state + hass.states.async_set("input_number.valve_duration", "invalid") + await hass.async_block_till_done() + assert acc.get_duration() == 0 + + # Test get_remaining_duration fallback with invalid state + hass.states.async_set("sensor.valve_end_time", "invalid") + await hass.async_block_till_done() + assert acc.get_remaining_duration() == 0 + + # Test get_remaining_duration with end time in the past + hass.states.async_set( + "sensor.valve_end_time", + (dt_util.utcnow() - timedelta(seconds=10)).isoformat(), + ) + await hass.async_block_till_done() + assert acc.get_remaining_duration() == 0 + + # Test set_duration with negative value + acc.set_duration(-10) + await hass.async_block_till_done() + assert acc.get_duration() == 0 + # Verify the service was called with correct parameters + assert len(call_set_value) == 1 + assert call_set_value[0].data == { + "entity_id": "input_number.valve_duration", + "value": -10, + } + + # Test set_duration with negative state + hass.states.async_set("sensor.valve_duration", -10) + await hass.async_block_till_done() + assert acc.get_duration() == 0 + + +async def test_valve_with_duration_characteristics( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: + """Test valve with set duration and remaining duration characteristics.""" + entity_id = "switch.sprinkler" + + # Test with duration and end time entities linked + hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set("input_number.valve_duration", "900") + hass.states.async_set("sensor.valve_end_time", dt_util.utcnow().isoformat()) + await hass.async_block_till_done() + + # Using Valve instead of ValveSwitch + acc = Valve( + hass, + hk_driver, + "Valve", + entity_id, + 5, + { + "linked_valve_duration": "input_number.valve_duration", + "linked_valve_end_time": "sensor.valve_end_time", + }, + ) + acc.run() + await hass.async_block_till_done() + + with freeze_time(dt_util.utcnow()): + hass.states.async_set( + "sensor.valve_end_time", + (dt_util.utcnow() + timedelta(seconds=600)).isoformat(), + ) + await hass.async_block_till_done() + assert acc.get_duration() == 900 + assert acc.get_remaining_duration() == 600 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 66906c72266..4cb8eb41489 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -15,6 +15,8 @@ from homeassistant.components.homekit.const import ( CONF_LINKED_BATTERY_SENSOR, CONF_LINKED_DOORBELL_SENSOR, CONF_LINKED_MOTION_SENSOR, + CONF_LINKED_VALVE_DURATION, + CONF_LINKED_VALVE_END_TIME, CONF_LOW_BATTERY_THRESHOLD, CONF_MAX_FPS, CONF_MAX_HEIGHT, @@ -128,7 +130,25 @@ def test_validate_entity_config() -> None: } }, {"switch.test": {CONF_TYPE: "invalid_type"}}, + { + "switch.test": { + CONF_TYPE: "sprinkler", + CONF_LINKED_VALVE_DURATION: "number.valve_duration", # Must be input_number entity + CONF_LINKED_VALVE_END_TIME: "datetime.valve_end_time", # Must be sensor (timestamp) entity + } + }, {"fan.test": {CONF_TYPE: "invalid_type"}}, + { + "valve.test": { + CONF_LINKED_VALVE_END_TIME: "datetime.valve_end_time", # Must be sensor (timestamp) entity + CONF_LINKED_VALVE_DURATION: "number.valve_duration", # Must be input_number + } + }, + { + "valve.test": { + CONF_TYPE: "sprinkler", # Extra keys not allowed + } + }, ] for conf in configs: @@ -212,6 +232,19 @@ def test_validate_entity_config() -> None: assert vec({"switch.demo": {CONF_TYPE: TYPE_VALVE}}) == { "switch.demo": {CONF_TYPE: TYPE_VALVE, CONF_LOW_BATTERY_THRESHOLD: 20} } + config = { + CONF_TYPE: TYPE_SPRINKLER, + CONF_LINKED_VALVE_DURATION: "input_number.valve_duration", + CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time", + } + assert vec({"switch.sprinkler": config}) == { + "switch.sprinkler": { + CONF_TYPE: TYPE_SPRINKLER, + CONF_LINKED_VALVE_DURATION: "input_number.valve_duration", + CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time", + CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD, + } + } assert vec({"sensor.co": {CONF_THRESHOLD_CO: 500}}) == { "sensor.co": {CONF_THRESHOLD_CO: 500, CONF_LOW_BATTERY_THRESHOLD: 20} } @@ -244,6 +277,17 @@ def test_validate_entity_config() -> None: CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD, } } + config = { + CONF_LINKED_VALVE_DURATION: "input_number.valve_duration", + CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time", + } + assert vec({"valve.demo": config}) == { + "valve.demo": { + CONF_LINKED_VALVE_DURATION: "input_number.valve_duration", + CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time", + CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD, + } + } def test_validate_media_player_features() -> None: From 4d59e8cd800aec7007c73d551438504d7f364aac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Aug 2025 07:49:51 +0200 Subject: [PATCH 179/265] Fix flaky velbus test (#149743) --- tests/components/velbus/conftest.py | 5 +- .../velbus/snapshots/test_init.ambr | 152 ++++++++++++------ tests/components/velbus/test_init.py | 3 +- 3 files changed, 112 insertions(+), 48 deletions(-) diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index f7cbeb7a052..d909480c8ea 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -97,6 +97,7 @@ def mock_module_subdevices() -> AsyncMock: """Mock a velbus module.""" module = AsyncMock(spec=Module) module.get_type_name.return_value = "VMB2BLE" + module.get_type.return_value = "123" module.get_addresses.return_value = [88] module.get_name.return_value = "Kitchen" module.get_serial.return_value = "a1b2c3d4e5f6" @@ -138,7 +139,7 @@ def mock_temperature() -> AsyncMock: channel.get_module_sw_version.return_value = "3.0.0" channel.get_module_serial.return_value = "asdfghjk" channel.get_module_type.return_value = 1 - channel.is_sub_device.return_value = False + channel.is_sub_device.return_value = True channel.is_counter_channel.return_value = False channel.get_class.return_value = "temperature" channel.get_unit.return_value = "°C" @@ -184,7 +185,7 @@ def mock_select() -> AsyncMock: channel.get_full_name.return_value = "Kitchen" channel.get_module_sw_version.return_value = "1.1.1" channel.get_module_serial.return_value = "qwerty1234567" - channel.is_sub_device.return_value = False + channel.is_sub_device.return_value = True channel.get_options.return_value = ["none", "summer", "winter", "holiday"] channel.get_selected_program.return_value = "winter" return channel diff --git a/tests/components/velbus/snapshots/test_init.ambr b/tests/components/velbus/snapshots/test_init.ambr index 1e17753a02f..037ab7e6236 100644 --- a/tests/components/velbus/snapshots/test_init.ambr +++ b/tests/components/velbus/snapshots/test_init.ambr @@ -46,7 +46,38 @@ 'identifiers': set({ tuple( 'velbus', - '88-9', + '2', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB7IN', + 'model_id': '8', + 'name': 'Input', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6', + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88', ), }), 'is_new': False, @@ -54,13 +85,44 @@ }), 'manufacturer': 'Velleman', 'model': 'VMB2BLE', - 'model_id': '10', - 'name': 'Basement', + 'model_id': '123', + 'name': 'Kitchen (VMB2BLE)', 'name_by_user': None, 'primary_config_entry': , - 'serial_number': '1234', + 'serial_number': 'a1b2c3d4e5f6', 'suggested_area': None, - 'sw_version': '1.0.1', + 'sw_version': '2.0.0', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-10', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMBDN1', + 'model_id': '9', + 'name': 'Dimmer full name', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6g7', + 'suggested_area': None, + 'sw_version': '1.0.0', 'via_device_id': , }), DeviceRegistryEntrySnapshot({ @@ -94,37 +156,6 @@ 'sw_version': '1.0.1', 'via_device_id': , }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'velbus', - '88-10', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Velleman', - 'model': 'VMBDN1', - 'model_id': '9', - 'name': 'Dimmer full name', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': 'a1b2c3d4e5f6g7', - 'suggested_area': None, - 'sw_version': '1.0.0', - 'via_device_id': , - }), DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -170,7 +201,7 @@ 'identifiers': set({ tuple( 'velbus', - '88', + '88-3', ), }), 'is_new': False, @@ -185,7 +216,7 @@ 'serial_number': 'asdfghjk', 'suggested_area': None, 'sw_version': '3.0.0', - 'via_device_id': None, + 'via_device_id': , }), DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -201,22 +232,22 @@ 'identifiers': set({ tuple( 'velbus', - '2', + '88-33', ), }), 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', - 'model': 'VMB7IN', - 'model_id': '8', - 'name': 'Input', + 'model': 'VMB4RYNO', + 'model_id': '3', + 'name': 'Kitchen', 'name_by_user': None, 'primary_config_entry': , - 'serial_number': 'a1b2c3d4e5f6', + 'serial_number': 'qwerty1234567', 'suggested_area': None, - 'sw_version': '1.0.0', - 'via_device_id': None, + 'sw_version': '1.1.1', + 'via_device_id': , }), DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -249,5 +280,36 @@ 'sw_version': '1.0.1', 'via_device_id': , }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-9', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB2BLE', + 'model_id': '10', + 'name': 'Basement', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '1234', + 'suggested_area': None, + 'sw_version': '1.0.1', + 'via_device_id': , + }), ]) # --- diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index 2d28ba81cb1..fc9046f977f 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -176,7 +176,8 @@ async def test_device_registry( device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) - assert device_entries == snapshot + # Sort by identifier to ensure consistent order in snapshot + assert sorted(device_entries, key=lambda x: list(x.identifiers)[0][1]) == snapshot device_parent = device_registry.async_get_device(identifiers={(DOMAIN, "88")}) assert device_parent.via_device_id is None From 8b53b263338584f5e2246acaaaf56c65e1719410 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 1 Aug 2025 09:13:53 +0200 Subject: [PATCH 180/265] Fix tuya light supported color modes (#149793) Co-authored-by: Erik --- homeassistant/components/tuya/light.py | 34 ++-- tests/components/tuya/__init__.py | 6 + .../tuya/fixtures/tyndj_pyakuuoc.json | 145 ++++++++++++++++++ .../components/tuya/snapshots/test_light.ambr | 56 +++++++ .../tuya/snapshots/test_sensor.ambr | 101 ++++++++++++ .../tuya/snapshots/test_switch.ambr | 48 ++++++ 6 files changed, 377 insertions(+), 13 deletions(-) create mode 100644 tests/components/tuya/fixtures/tyndj_pyakuuoc.json diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index cb7555c38d8..7b73e825900 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -16,6 +16,7 @@ from homeassistant.components.light import ( ColorMode, LightEntity, LightEntityDescription, + color_supported, filter_supported_color_modes, ) from homeassistant.const import EntityCategory @@ -530,19 +531,6 @@ class TuyaLightEntity(TuyaEntity, LightEntity): description.brightness_min, dptype=DPType.INTEGER ) - if int_type := self.find_dpcode( - description.color_temp, dptype=DPType.INTEGER, prefer_function=True - ): - self._color_temp = int_type - color_modes.add(ColorMode.COLOR_TEMP) - # If entity does not have color_temp, check if it has work_mode "white" - elif color_mode_enum := self.find_dpcode( - description.color_mode, dptype=DPType.ENUM, prefer_function=True - ): - if WorkMode.WHITE.value in color_mode_enum.range: - color_modes.add(ColorMode.WHITE) - self._white_color_mode = ColorMode.WHITE - if ( dpcode := self.find_dpcode(description.color_data, prefer_function=True) ) and self.get_dptype(dpcode) == DPType.JSON: @@ -568,6 +556,26 @@ class TuyaLightEntity(TuyaEntity, LightEntity): ): self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2 + # Check if the light has color temperature + if int_type := self.find_dpcode( + description.color_temp, dptype=DPType.INTEGER, prefer_function=True + ): + self._color_temp = int_type + color_modes.add(ColorMode.COLOR_TEMP) + # If light has color but does not have color_temp, check if it has + # work_mode "white" + elif ( + color_supported(color_modes) + and ( + color_mode_enum := self.find_dpcode( + description.color_mode, dptype=DPType.ENUM, prefer_function=True + ) + ) + and WorkMode.WHITE.value in color_mode_enum.range + ): + color_modes.add(ColorMode.WHITE) + self._white_color_mode = ColorMode.WHITE + self._attr_supported_color_modes = filter_supported_color_modes(color_modes) if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 039b8f29290..d793b87854a 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -149,6 +149,12 @@ DEVICE_MOCKS = { Platform.SELECT, Platform.SWITCH, ], + "tyndj_pyakuuoc": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, + ], "wk_air_conditioner": [ # https://github.com/home-assistant/core/issues/146263 Platform.CLIMATE, diff --git a/tests/components/tuya/fixtures/tyndj_pyakuuoc.json b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json new file mode 100644 index 00000000000..973cecabc0b --- /dev/null +++ b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json @@ -0,0 +1,145 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "1753247726209KOaaPc", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfdb773e4ae317e3915h2i", + "name": "Solar zijpad", + "category": "tyndj", + "product_id": "pyakuuoc", + "product_name": "Solar flood light App panel", + "online": false, + "sub": true, + "time_zone": "+08:00", + "active_time": "2023-03-08T13:24:06+00:00", + "create_time": "2023-03-08T13:24:06+00:00", + "update_time": "2023-03-08T13:24:06+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "countdown": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "switch_save_energy": { + "type": "Boolean", + "value": {} + }, + "device_mode": { + "type": "Enum", + "value": { + "range": ["manual", "auto"] + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "countdown": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "switch_save_energy": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "device_mode": { + "type": "Enum", + "value": { + "range": ["manual", "auto"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value": 10, + "scene_data": "", + "countdown": 0, + "switch_save_energy": false, + "battery_percentage": 0, + "device_mode": "manual", + "battery_state": "low" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 5fcf58dda6d..ec8e663f62c 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -249,3 +249,59 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.solar_zijpad', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfdb773e4ae317e3915h2iswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.solar_zijpad', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 57e73eccda5..80051a08396 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -2233,6 +2233,107 @@ 'state': '0.0', }) # --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solar_zijpad_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2ibattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Solar zijpad Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solar_zijpad_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solar_zijpad_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2ibattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad Battery state', + }), + 'context': , + 'entity_id': 'sensor.solar_zijpad_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 71aa05329aa..e21fe9c91bd 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1161,6 +1161,54 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][switch.solar_zijpad_energy_saving-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.solar_zijpad_energy_saving', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Energy saving', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_saving', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2iswitch_save_energy', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][switch.solar_zijpad_energy_saving-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad Energy saving', + }), + 'context': , + 'entity_id': 'switch.solar_zijpad_energy_saving', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[wk_air_conditioner][switch.clima_cucina_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 22e054f4cd1e5a24cf890412187f23a6c23a5c63 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 1 Aug 2025 09:24:22 +0200 Subject: [PATCH 181/265] Add diagnostics to UISP AirOS (#149631) --- homeassistant/components/airos/diagnostics.py | 33 + .../components/airos/quality_scale.yaml | 2 +- .../airos/snapshots/test_diagnostics.ambr | 623 ++++++++++++++++++ tests/components/airos/test_diagnostics.py | 32 + 4 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airos/diagnostics.py create mode 100644 tests/components/airos/snapshots/test_diagnostics.ambr create mode 100644 tests/components/airos/test_diagnostics.py diff --git a/homeassistant/components/airos/diagnostics.py b/homeassistant/components/airos/diagnostics.py new file mode 100644 index 00000000000..70fef685c86 --- /dev/null +++ b/homeassistant/components/airos/diagnostics.py @@ -0,0 +1,33 @@ +"""Diagnostics support for airOS.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from .coordinator import AirOSConfigEntry + +IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related +HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address +TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD] +TO_REDACT_AIROS = [ + "hostname", # Prevent leaking device naming + "essid", # Network SSID + "lat", # GPS latitude to prevent exposing location data. + "lon", # GPS longitude to prevent exposing location data. + *HW_REDACT, + *IP_REDACT, +] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: AirOSConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return { + "entry_data": async_redact_data(entry.data, TO_REDACT_HA), + "data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS), + } diff --git a/homeassistant/components/airos/quality_scale.yaml b/homeassistant/components/airos/quality_scale.yaml index a0bacd5ebba..c8c5d209af5 100644 --- a/homeassistant/components/airos/quality_scale.yaml +++ b/homeassistant/components/airos/quality_scale.yaml @@ -41,7 +41,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: todo discovery: todo docs-data-update: done diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..bc2dedc905a --- /dev/null +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -0,0 +1,623 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'chain_names': list([ + dict({ + 'name': 'Chain 0', + 'number': 1, + }), + dict({ + 'name': 'Chain 1', + 'number': 2, + }), + ]), + 'derived': dict({ + 'mac': '**REDACTED**', + 'mac_interface': 'br0', + }), + 'firewall': dict({ + 'eb6tables': False, + 'ebtables': False, + 'ip6tables': False, + 'iptables': False, + }), + 'genuine': '/images/genuine.png', + 'gps': dict({ + 'fix': 0, + 'lat': '**REDACTED**', + 'lon': '**REDACTED**', + }), + 'host': dict({ + 'cpuload': 10.10101, + 'device_id': '03aa0d0b40fed0a47088293584ef5432', + 'devmodel': 'NanoStation 5AC loco', + 'freeram': 16564224, + 'fwversion': 'v8.7.17', + 'height': 3, + 'hostname': '**REDACTED**', + 'loadavg': 0.412598, + 'netrole': 'bridge', + 'power_time': 268683, + 'temperature': 0, + 'time': '2025-06-23 23:06:42', + 'timestamp': 2668313184, + 'totalram': 63447040, + 'uptime': 264888, + }), + 'interfaces': list([ + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'eth0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': 18, + 'duplex': True, + 'ip6addr': None, + 'ipaddr': '**REDACTED**', + 'plugged': True, + 'rx_bytes': 3984971949, + 'rx_dropped': 0, + 'rx_errors': 4, + 'rx_packets': 73564835, + 'snr': list([ + 30, + 30, + 30, + 30, + ]), + 'speed': 1000, + 'tx_bytes': 209900085624, + 'tx_dropped': 10, + 'tx_errors': 0, + 'tx_packets': 185866883, + }), + }), + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'ath0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': None, + 'duplex': False, + 'ip6addr': None, + 'ipaddr': '**REDACTED**', + 'plugged': False, + 'rx_bytes': 206938324766, + 'rx_dropped': 0, + 'rx_errors': 0, + 'rx_packets': 149767200, + 'snr': None, + 'speed': 0, + 'tx_bytes': 5265602738, + 'tx_dropped': 2005, + 'tx_errors': 0, + 'tx_packets': 52980390, + }), + }), + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'br0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': None, + 'duplex': False, + 'ip6addr': '**REDACTED**', + 'ipaddr': '**REDACTED**', + 'plugged': True, + 'rx_bytes': 204802727, + 'rx_dropped': 0, + 'rx_errors': 0, + 'rx_packets': 1791592, + 'snr': None, + 'speed': 0, + 'tx_bytes': 236295176, + 'tx_dropped': 0, + 'tx_errors': 0, + 'tx_packets': 298119, + }), + }), + ]), + 'ntpclient': dict({ + }), + 'portfw': False, + 'provmode': dict({ + }), + 'services': dict({ + 'airview': 2, + 'dhcp6d_stateful': False, + 'dhcpc': False, + 'dhcpd': False, + 'pppoe': False, + }), + 'unms': dict({ + 'status': 0, + 'timestamp': None, + }), + 'wireless': dict({ + 'antenna_gain': 13, + 'apmac': '**REDACTED**', + 'aprepeater': False, + 'band': 2, + 'cac_state': 0, + 'cac_timeout': 0, + 'center1_freq': 5530, + 'chanbw': 80, + 'compat_11n': 0, + 'count': 1, + 'dfs': 1, + 'distance': 0, + 'essid': '**REDACTED**', + 'frequency': 5500, + 'hide_essid': 0, + 'ieeemode': '11ACVHT80', + 'mode': 'ap-ptp', + 'noisef': -89, + 'nol_state': 0, + 'nol_timeout': 0, + 'polling': dict({ + 'atpc_status': 2, + 'cb_capacity': 593970, + 'dl_capacity': 647400, + 'ff_cap_rep': False, + 'fixed_frame': False, + 'gps_sync': False, + 'rx_use': 42, + 'tx_use': 6, + 'ul_capacity': 540540, + 'use': 48, + }), + 'rstatus': 5, + 'rx_chainmask': 3, + 'rx_idx': 8, + 'rx_nss': 2, + 'security': 'WPA2', + 'service': dict({ + 'link': 266003, + 'time': 267181, + }), + 'sta': list([ + dict({ + 'airmax': dict({ + 'actual_priority': 0, + 'atpc_status': 2, + 'beam': 0, + 'cb_capacity': 593970, + 'desired_priority': 0, + 'dl_capacity': 647400, + 'rx': dict({ + 'cinr': 31, + 'evm': list([ + list([ + 31, + 28, + 33, + 32, + 32, + 32, + 31, + 31, + 31, + 29, + 30, + 32, + 30, + 27, + 34, + 31, + 31, + 30, + 32, + 29, + 31, + 29, + 31, + 33, + 31, + 31, + 32, + 30, + 31, + 34, + 33, + 31, + 30, + 31, + 30, + 31, + 31, + 32, + 31, + 30, + 33, + 31, + 30, + 31, + 27, + 31, + 30, + 30, + 30, + 30, + 30, + 29, + 32, + 34, + 31, + 30, + 28, + 30, + 29, + 35, + 31, + 33, + 32, + 29, + ]), + list([ + 34, + 34, + 35, + 34, + 35, + 35, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + 34, + 34, + 35, + 34, + 33, + 33, + 35, + 34, + 34, + 35, + 34, + 35, + 34, + 34, + 35, + 34, + 34, + 33, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + 35, + 34, + 35, + 33, + 34, + 34, + 34, + 34, + 35, + 35, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + ]), + ]), + 'usage': 42, + }), + 'tx': dict({ + 'cinr': 31, + 'evm': list([ + list([ + 32, + 34, + 28, + 33, + 35, + 30, + 31, + 33, + 30, + 30, + 32, + 30, + 29, + 33, + 31, + 29, + 33, + 31, + 31, + 30, + 33, + 34, + 33, + 31, + 33, + 32, + 32, + 31, + 29, + 31, + 30, + 32, + 31, + 30, + 29, + 32, + 31, + 32, + 31, + 31, + 32, + 29, + 31, + 29, + 30, + 32, + 32, + 31, + 32, + 32, + 33, + 31, + 28, + 29, + 31, + 31, + 33, + 32, + 33, + 32, + 32, + 32, + 31, + 33, + ]), + list([ + 37, + 37, + 37, + 38, + 38, + 37, + 36, + 38, + 38, + 37, + 37, + 37, + 37, + 37, + 39, + 37, + 37, + 37, + 37, + 37, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 37, + 38, + 37, + 37, + 38, + 37, + 37, + 37, + 38, + 37, + 38, + 37, + 37, + 37, + 37, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 38, + 37, + 37, + 38, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 37, + ]), + ]), + 'usage': 6, + }), + 'ul_capacity': 540540, + }), + 'airos_connected': True, + 'cb_capacity_expect': 416000, + 'chainrssi': list([ + 35, + 32, + 0, + ]), + 'distance': 1, + 'dl_avg_linkscore': 100, + 'dl_capacity_expect': 208000, + 'dl_linkscore': 100, + 'dl_rate_expect': 3, + 'dl_signal_expect': -80, + 'last_disc': 1, + 'lastip': '**REDACTED**', + 'mac': '**REDACTED**', + 'noisefloor': -89, + 'remote': dict({ + 'age': 1, + 'airview': 2, + 'antenna_gain': 13, + 'cable_loss': 0, + 'chainrssi': list([ + 33, + 37, + 0, + ]), + 'compat_11n': 0, + 'cpuload': 43.564301, + 'device_id': 'd4f4cdf82961e619328a8f72f8d7653b', + 'distance': 1, + 'ethlist': list([ + dict({ + 'cable_len': 14, + 'duplex': True, + 'enabled': True, + 'ifname': 'eth0', + 'plugged': True, + 'snr': list([ + 30, + 30, + 29, + 30, + ]), + 'speed': 1000, + }), + ]), + 'freeram': 14290944, + 'gps': dict({ + 'fix': 0, + 'lat': '**REDACTED**', + 'lon': '**REDACTED**', + }), + 'height': 2, + 'hostname': '**REDACTED**', + 'ip6addr': '**REDACTED**', + 'ipaddr': '**REDACTED**', + 'mode': 'sta-ptp', + 'netrole': 'bridge', + 'noisefloor': -90, + 'oob': False, + 'platform': 'NanoStation 5AC loco', + 'power_time': 268512, + 'rssi': 38, + 'rx_bytes': 3624206478, + 'rx_chainmask': 3, + 'rx_throughput': 251, + 'service': dict({ + 'link': 265996, + 'time': 267195, + }), + 'signal': -58, + 'sys_id': '0xe7fa', + 'temperature': 0, + 'time': '2025-06-23 23:13:54', + 'totalram': 63447040, + 'tx_bytes': 212308148210, + 'tx_power': -4, + 'tx_ratedata': list([ + 14, + 4, + 372, + 2223, + 4708, + 4037, + 8142, + 485763, + 29420892, + 24748154, + ]), + 'tx_throughput': 16023, + 'unms': dict({ + 'status': 0, + 'timestamp': None, + }), + 'uptime': 265320, + 'version': 'WA.ar934x.v8.7.17.48152.250620.2132', + }), + 'rssi': 37, + 'rx_idx': 8, + 'rx_nss': 2, + 'signal': -59, + 'stats': dict({ + 'rx_bytes': 206938324814, + 'rx_packets': 149767200, + 'rx_pps': 846, + 'tx_bytes': 5265602739, + 'tx_packets': 52980390, + 'tx_pps': 0, + }), + 'tx_idx': 9, + 'tx_latency': 0, + 'tx_lretries': 0, + 'tx_nss': 2, + 'tx_packets': 0, + 'tx_ratedata': list([ + 175, + 4, + 47, + 200, + 673, + 158, + 163, + 138, + 68895, + 19577430, + ]), + 'tx_sretries': 0, + 'ul_avg_linkscore': 88, + 'ul_capacity_expect': 624000, + 'ul_linkscore': 86, + 'ul_rate_expect': 8, + 'ul_signal_expect': -55, + 'uptime': 170281, + }), + ]), + 'sta_disconnected': list([ + ]), + 'throughput': dict({ + 'rx': 9907, + 'tx': 222, + }), + 'tx_chainmask': 3, + 'tx_idx': 9, + 'tx_nss': 2, + 'txpower': -3, + }), + }), + 'entry_data': dict({ + 'host': '**REDACTED**', + 'password': '**REDACTED**', + 'username': 'ubnt', + }), + }) +# --- diff --git a/tests/components/airos/test_diagnostics.py b/tests/components/airos/test_diagnostics.py new file mode 100644 index 00000000000..453e8ff1f03 --- /dev/null +++ b/tests/components/airos/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Diagnostic tests for airOS.""" + +from unittest.mock import MagicMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.airos.coordinator import AirOSData +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_airos_client: MagicMock, + mock_config_entry: MockConfigEntry, + ap_fixture: AirOSData, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + await setup_integration(hass, mock_config_entry) + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) From 0d7608f7c5d46a956120a685e39ce384057fcd29 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Aug 2025 10:34:34 +0200 Subject: [PATCH 182/265] Deprecate DeviceEntry.suggested_area (#149730) --- .../components/analytics/analytics.py | 1 - .../components/enphase_envoy/diagnostics.py | 2 + homeassistant/helpers/device_registry.py | 16 +- .../components/acaia/snapshots/test_init.ambr | 1 - .../airgradient/snapshots/test_init.ambr | 2 - .../alexa_devices/snapshots/test_init.ambr | 1 - tests/components/analytics/test_analytics.py | 2 - .../aosmith/snapshots/test_device.ambr | 1 - .../apcupsd/snapshots/test_init.ambr | 4 - .../august/snapshots/test_binary_sensor.ambr | 1 - .../august/snapshots/test_lock.ambr | 1 - tests/components/axis/snapshots/test_hub.ambr | 2 - tests/components/bond/test_init.py | 18 +- .../cambridge_audio/snapshots/test_init.ambr | 1 - .../components/deconz/snapshots/test_hub.ambr | 1 - .../snapshots/test_init.ambr | 3 - .../ecovacs/snapshots/test_init.ambr | 1 - .../elgato/snapshots/test_button.ambr | 2 - .../elgato/snapshots/test_light.ambr | 3 - .../elgato/snapshots/test_sensor.ambr | 5 - .../elgato/snapshots/test_switch.ambr | 2 - .../snapshots/test_diagnostics.ambr | 8 - tests/components/esphome/test_manager.py | 35 ++- tests/components/flo/snapshots/test_init.ambr | 2 - .../snapshots/test_init.ambr | 1 - .../snapshots/test_init.ambr | 4 - .../components/homee/snapshots/test_init.ambr | 2 - .../snapshots/test_init.ambr | 116 --------- .../homekit_controller/test_init.py | 2 + .../homewizard/snapshots/test_button.ambr | 1 - .../homewizard/snapshots/test_number.ambr | 2 - .../homewizard/snapshots/test_select.ambr | 1 - .../homewizard/snapshots/test_sensor.ambr | 231 ------------------ .../homewizard/snapshots/test_switch.ambr | 11 - tests/components/hue/test_light_v1.py | 21 +- .../snapshots/test_init.ambr | 1 - .../snapshots/test_init.ambr | 1 - .../iotty/snapshots/test_switch.ambr | 1 - .../ista_ecotrend/snapshots/test_init.ambr | 2 - .../ituran/snapshots/test_init.ambr | 1 - .../kitchen_sink/snapshots/test_switch.ambr | 4 - .../lamarzocco/snapshots/test_init.ambr | 1 - .../lektrico/snapshots/test_init.ambr | 1 - tests/components/lifx/test_config_flow.py | 9 +- .../mastodon/snapshots/test_init.ambr | 1 - .../mealie/snapshots/test_init.ambr | 1 - .../meater/snapshots/test_init.ambr | 1 - .../components/miele/snapshots/test_init.ambr | 1 - tests/components/mqtt/common.py | 20 +- .../myuplink/snapshots/test_init.ambr | 3 - .../netatmo/snapshots/test_init.ambr | 39 --- .../netgear_lte/snapshots/test_init.ambr | 1 - tests/components/nut/test_init.py | 8 +- .../nyt_games/snapshots/test_init.ambr | 3 - .../components/ohme/snapshots/test_init.ambr | 1 - .../ondilo_ico/snapshots/test_init.ambr | 2 - .../onedrive/snapshots/test_init.ambr | 1 - .../onewire/snapshots/test_init.ambr | 22 -- .../snapshots/test_init.ambr | 2 - .../overseerr/snapshots/test_init.ambr | 1 - .../palazzetti/snapshots/test_init.ambr | 1 - .../peblar/snapshots/test_init.ambr | 1 - .../rainforest_raven/snapshots/test_init.ambr | 1 - .../renault/snapshots/test_init.ambr | 5 - tests/components/roku/test_binary_sensor.py | 13 +- tests/components/roku/test_media_player.py | 13 +- tests/components/roku/test_sensor.py | 13 +- .../components/rova/snapshots/test_init.ambr | 1 - .../russound_rio/snapshots/test_init.ambr | 1 - .../samsungtv/snapshots/test_init.ambr | 3 - .../schlage/snapshots/test_init.ambr | 1 - .../sensibo/snapshots/test_entity.ambr | 4 - .../sfr_box/snapshots/test_binary_sensor.ambr | 2 - .../sfr_box/snapshots/test_button.ambr | 1 - .../sfr_box/snapshots/test_sensor.ambr | 1 - .../slide_local/snapshots/test_init.ambr | 1 - .../smartthings/snapshots/test_init.ambr | 68 ------ .../smarty/snapshots/test_init.ambr | 1 - .../smlight/snapshots/test_init.ambr | 1 - tests/components/sonos/test_media_player.py | 12 +- .../squeezebox/snapshots/test_init.ambr | 2 - .../snapshots/test_binary_sensor.ambr | 2 - .../tailwind/snapshots/test_button.ambr | 1 - .../tailwind/snapshots/test_cover.ambr | 2 - .../tailwind/snapshots/test_number.ambr | 1 - .../components/tedee/snapshots/test_init.ambr | 2 - .../components/tedee/snapshots/test_lock.ambr | 1 - .../tesla_fleet/snapshots/test_init.ambr | 4 - .../teslemetry/snapshots/test_init.ambr | 4 - .../components/tile/snapshots/test_init.ambr | 1 - .../tplink/snapshots/test_binary_sensor.ambr | 1 - .../tplink/snapshots/test_button.ambr | 1 - .../tplink/snapshots/test_camera.ambr | 1 - .../tplink/snapshots/test_climate.ambr | 1 - .../components/tplink/snapshots/test_fan.ambr | 1 - .../tplink/snapshots/test_number.ambr | 1 - .../tplink/snapshots/test_select.ambr | 1 - .../tplink/snapshots/test_sensor.ambr | 1 - .../tplink/snapshots/test_siren.ambr | 1 - .../tplink/snapshots/test_switch.ambr | 1 - .../tplink/snapshots/test_vacuum.ambr | 1 - .../components/tuya/snapshots/test_init.ambr | 1 - .../twentemilieu/snapshots/test_calendar.ambr | 1 - .../twentemilieu/snapshots/test_sensor.ambr | 5 - .../uptime/snapshots/test_sensor.ambr | 1 - .../velbus/snapshots/test_init.ambr | 10 - .../components/vesync/snapshots/test_fan.ambr | 12 - .../vesync/snapshots/test_light.ambr | 12 - .../vesync/snapshots/test_sensor.ambr | 12 - .../vesync/snapshots/test_switch.ambr | 12 - .../webostv/snapshots/test_media_player.ambr | 1 - .../whois/snapshots/test_sensor.ambr | 10 - .../withings/snapshots/test_init.ambr | 2 - .../wled/snapshots/test_button.ambr | 1 - .../wled/snapshots/test_number.ambr | 2 - .../wled/snapshots/test_select.ambr | 4 - .../wled/snapshots/test_switch.ambr | 4 - .../wmspro/snapshots/test_cover.ambr | 1 - .../wmspro/snapshots/test_init.ambr | 12 - .../wmspro/snapshots/test_light.ambr | 1 - .../wmspro/snapshots/test_scene.ambr | 1 - .../wolflink/snapshots/test_sensor.ambr | 1 - tests/components/wyoming/test_devices.py | 5 +- .../yale/snapshots/test_binary_sensor.ambr | 1 - .../components/yale/snapshots/test_lock.ambr | 1 - .../snapshots/test_entity_platform.ambr | 2 - tests/helpers/test_device_registry.py | 48 +++- tests/syrupy.py | 2 + 128 files changed, 180 insertions(+), 795 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 8a2a182c796..0d0f5183566 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -430,7 +430,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: "model": device.model, "sw_version": device.sw_version, "hw_version": device.hw_version, - "has_suggested_area": device.suggested_area is not None, "has_configuration_url": device.configuration_url is not None, "via_device": None, } diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index a1a9d4ed6b4..6487830675f 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -116,6 +116,8 @@ async def async_get_config_entry_diagnostics( entities.append({"entity": entity_dict, "state": state_dict}) device_dict = asdict(device) device_dict.pop("_cache", None) + # This can be removed when suggested_area is removed from DeviceEntry + device_dict.pop("_suggested_area") device_entities.append({"device": device_dict, "entities": entities}) # remove envoy serial diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c8b4428a7cc..d3866d8c9c3 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -32,6 +32,7 @@ from homeassistant.util.json import format_unserializable_data from . import storage, translation from .debounce import Debouncer +from .deprecation import deprecated_function from .frame import ReportBehavior, report_usage from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment from .registry import BaseRegistry, BaseRegistryItems, RegistryIndexType @@ -67,6 +68,7 @@ CONNECTION_ZIGBEE = "zigbee" ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 +# Can be removed when suggested_area is removed from DeviceEntry RUNTIME_ONLY_ATTRS = {"suggested_area"} CONFIGURATION_URL_SCHEMES = {"http", "https", "homeassistant"} @@ -343,7 +345,8 @@ class DeviceEntry: name: str | None = attr.ib(default=None) primary_config_entry: str | None = attr.ib(default=None) serial_number: str | None = attr.ib(default=None) - suggested_area: str | None = attr.ib(default=None) + # Suggested area is deprecated and will be removed from DeviceEntry in 2026.9. + _suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) via_device_id: str | None = attr.ib(default=None) # This value is not stored, just used to keep track of events to fire. @@ -442,6 +445,14 @@ class DeviceEntry: ) ) + @property + @deprecated_function( + "code which ignores suggested_area", breaks_in_ha_version="2026.9" + ) + def suggested_area(self) -> str | None: + """Return the suggested area for this device entry.""" + return self._suggested_area + @attr.s(frozen=True, slots=True) class DeletedDeviceEntry: @@ -1197,6 +1208,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ("name", name), ("name_by_user", name_by_user), ("serial_number", serial_number), + # Can be removed when suggested_area is removed from DeviceEntry ("suggested_area", suggested_area), ("sw_version", sw_version), ("via_device_id", via_device_id), @@ -1211,6 +1223,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if not new_values: return old + # This condition can be removed when suggested_area is removed from DeviceEntry if not RUNTIME_ONLY_ATTRS.issuperset(new_values): # Change modified_at if we are changing something that we store new_values["modified_at"] = utcnow() @@ -1233,6 +1246,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): # firing events for data we have nothing to compare # against since its never saved on disk if RUNTIME_ONLY_ATTRS.issuperset(new_values): + # This can be removed when suggested_area is removed from DeviceEntry return new self.async_schedule_save() diff --git a/tests/components/acaia/snapshots/test_init.ambr b/tests/components/acaia/snapshots/test_init.ambr index c7a11cb58df..d518de056b2 100644 --- a/tests/components/acaia/snapshots/test_init.ambr +++ b/tests/components/acaia/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Kitchen', 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index b3181fddfeb..96ce43260aa 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '84fce612f5b8', - 'suggested_area': None, 'sw_version': '3.1.1', 'via_device_id': None, }) @@ -68,7 +67,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '84fce612f5b8', - 'suggested_area': None, 'sw_version': '3.1.1', 'via_device_id': None, }) diff --git a/tests/components/alexa_devices/snapshots/test_init.ambr b/tests/components/alexa_devices/snapshots/test_init.ambr index e0460c4c173..c396c65246a 100644 --- a/tests/components/alexa_devices/snapshots/test_init.ambr +++ b/tests/components/alexa_devices/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'echo_test_serial_number', - 'suggested_area': None, 'sw_version': 'echo_test_software_version', 'via_device_id': None, }) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 90f3049d8fd..0e14d556620 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1051,7 +1051,6 @@ async def test_devices_payload( "hw_version": "test-hw-version", "integration": "hue", "is_custom_integration": False, - "has_suggested_area": True, "has_configuration_url": True, "via_device": None, }, @@ -1063,7 +1062,6 @@ async def test_devices_payload( "hw_version": None, "integration": "hue", "is_custom_integration": False, - "has_suggested_area": False, "has_configuration_url": False, "via_device": 0, }, diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index e647b7fa6a5..f814106870b 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'serial', - 'suggested_area': 'Basement', 'sw_version': '2.14', 'via_device_id': None, }) diff --git a/tests/components/apcupsd/snapshots/test_init.ambr b/tests/components/apcupsd/snapshots/test_init.ambr index 39f28b528fc..6ca412f7e34 100644 --- a/tests/components/apcupsd/snapshots/test_init.ambr +++ b/tests/components/apcupsd/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.14.14 (31 May 2016) unknown', 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/august/snapshots/test_binary_sensor.ambr b/tests/components/august/snapshots/test_binary_sensor.ambr index be5947372f5..c9a7b7ba039 100644 --- a/tests/components/august/snapshots/test_binary_sensor.ambr +++ b/tests/components/august/snapshots/test_binary_sensor.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'tmt100 Name', 'sw_version': '3.1.0-HYDRC75+201909251139', 'via_device_id': None, }) diff --git a/tests/components/august/snapshots/test_lock.ambr b/tests/components/august/snapshots/test_lock.ambr index 0a594fed1ee..eb2cf7a815a 100644 --- a/tests/components/august/snapshots/test_lock.ambr +++ b/tests/components/august/snapshots/test_lock.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'online_with_doorsense Name', 'sw_version': 'undefined-4.3.0-1.8.14', 'via_device_id': None, }) diff --git a/tests/components/axis/snapshots/test_hub.ambr b/tests/components/axis/snapshots/test_hub.ambr index 9e407bfef0b..ab4745011dd 100644 --- a/tests/components/axis/snapshots/test_hub.ambr +++ b/tests/components/axis/snapshots/test_hub.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '00:40:8c:12:34:56', - 'suggested_area': None, 'sw_version': '9.10.1', 'via_device_id': None, }) @@ -68,7 +67,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '00:40:8c:12:34:56', - 'suggested_area': None, 'sw_version': '9.80.1', 'via_device_id': None, }) diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 0aaff0edfe7..c8ced85c933 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -11,7 +11,11 @@ from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ASSUMED_STATE, CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from .common import ( @@ -202,7 +206,9 @@ async def test_old_identifiers_are_removed( async def test_smart_by_bond_device_suggested_area( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test we can setup a smart by bond device and get the suggested area.""" config_entry = MockConfigEntry( @@ -241,11 +247,13 @@ async def test_smart_by_bond_device_suggested_area( device = device_registry.async_get_device(identifiers={(DOMAIN, "KXXX12345")}) assert device is not None - assert device.suggested_area == "Den" + assert device.area_id == area_registry.async_get_area_by_name("Den").id async def test_bridge_device_suggested_area( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test we can setup a bridge bond device and get the suggested area.""" config_entry = MockConfigEntry( @@ -289,7 +297,7 @@ async def test_bridge_device_suggested_area( device = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert device is not None - assert device.suggested_area == "Office" + assert device.area_id == area_registry.async_get_area_by_name("Office").id async def test_device_remove_devices( diff --git a/tests/components/cambridge_audio/snapshots/test_init.ambr b/tests/components/cambridge_audio/snapshots/test_init.ambr index 7f4bbed36f7..71a54cdb001 100644 --- a/tests/components/cambridge_audio/snapshots/test_init.ambr +++ b/tests/components/cambridge_audio/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0020c2d8', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/deconz/snapshots/test_hub.ambr b/tests/components/deconz/snapshots/test_hub.ambr index 06067b69c17..b171dafbd5d 100644 --- a/tests/components/deconz/snapshots/test_hub.ambr +++ b/tests/components/deconz/snapshots/test_hub.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/devolo_home_network/snapshots/test_init.ambr b/tests/components/devolo_home_network/snapshots/test_init.ambr index 27ffd981b1e..4f965ce8d05 100644 --- a/tests/components/devolo_home_network/snapshots/test_init.ambr +++ b/tests/components/devolo_home_network/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) @@ -68,7 +67,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) @@ -101,7 +99,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index e403c937394..642f0db6813 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'E1234567890000000001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index 2f1c2107b52..5ff3710dfd7 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -80,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -166,7 +165,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index 16f20224079..8ee893f6be5 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -112,7 +112,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) @@ -232,7 +231,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) @@ -352,7 +350,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index 3592e88f975..ebf98ff02ae 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -87,7 +87,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -183,7 +182,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -279,7 +277,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -372,7 +369,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -468,7 +464,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index f29c16d0cae..8c75ed137b1 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -79,7 +79,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -164,7 +163,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 3a7f4e4fb9f..be638168b34 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -60,7 +60,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -308,7 +307,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -939,7 +937,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -1187,7 +1184,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -1862,7 +1858,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -2110,7 +2105,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -2806,7 +2800,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -3356,7 +3349,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 8d2dd211869..fec957a9560 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -50,6 +50,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( + area_registry as ar, device_registry as dr, entity_registry as er, issue_registry as ir, @@ -1170,6 +1171,7 @@ async def test_esphome_user_services_changes( async def test_esphome_device_with_suggested_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, @@ -1184,11 +1186,12 @@ async def test_esphome_device_with_suggested_area( dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} ) - assert dev.suggested_area == "kitchen" + assert dev.area_id == area_registry.async_get_area_by_name("kitchen").id async def test_esphome_device_area_priority( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, @@ -1207,7 +1210,7 @@ async def test_esphome_device_area_priority( connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} ) # Should use device_info.area.name instead of suggested_area - assert dev.suggested_area == "Living Room" + assert dev.area_id == area_registry.async_get_area_by_name("Living Room").id async def test_esphome_device_with_project( @@ -1535,6 +1538,7 @@ async def test_assist_in_progress_issue_deleted( async def test_sub_device_creation( hass: HomeAssistant, + area_registry: ar.AreaRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, ) -> None: @@ -1571,7 +1575,7 @@ async def test_sub_device_creation( connections={(dr.CONNECTION_NETWORK_MAC, device.device_info.mac_address)} ) assert main_device is not None - assert main_device.suggested_area == "Main Hub" + assert main_device.area_id == area_registry.async_get_area_by_name("Main Hub").id # Check sub devices are created sub_device_1 = device_registry.async_get_device( @@ -1579,7 +1583,9 @@ async def test_sub_device_creation( ) assert sub_device_1 is not None assert sub_device_1.name == "Motion Sensor" - assert sub_device_1.suggested_area == "Living Room" + assert ( + sub_device_1.area_id == area_registry.async_get_area_by_name("Living Room").id + ) assert sub_device_1.via_device_id == main_device.id sub_device_2 = device_registry.async_get_device( @@ -1587,7 +1593,9 @@ async def test_sub_device_creation( ) assert sub_device_2 is not None assert sub_device_2.name == "Light Switch" - assert sub_device_2.suggested_area == "Living Room" + assert ( + sub_device_2.area_id == area_registry.async_get_area_by_name("Living Room").id + ) assert sub_device_2.via_device_id == main_device.id sub_device_3 = device_registry.async_get_device( @@ -1595,7 +1603,7 @@ async def test_sub_device_creation( ) assert sub_device_3 is not None assert sub_device_3.name == "Temperature Sensor" - assert sub_device_3.suggested_area == "Bedroom" + assert sub_device_3.area_id == area_registry.async_get_area_by_name("Bedroom").id assert sub_device_3.via_device_id == main_device.id @@ -1731,6 +1739,7 @@ async def test_sub_device_with_empty_name( async def test_sub_device_references_main_device_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, ) -> None: @@ -1772,28 +1781,34 @@ async def test_sub_device_references_main_device_area( connections={(dr.CONNECTION_NETWORK_MAC, device.device_info.mac_address)} ) assert main_device is not None - assert main_device.suggested_area == "Main Hub Area" + assert ( + main_device.area_id == area_registry.async_get_area_by_name("Main Hub Area").id + ) # Check sub device 1 uses main device's area sub_device_1 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_11111111")} ) assert sub_device_1 is not None - assert sub_device_1.suggested_area == "Main Hub Area" + assert ( + sub_device_1.area_id == area_registry.async_get_area_by_name("Main Hub Area").id + ) # Check sub device 2 uses Living Room sub_device_2 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_22222222")} ) assert sub_device_2 is not None - assert sub_device_2.suggested_area == "Living Room" + assert ( + sub_device_2.area_id == area_registry.async_get_area_by_name("Living Room").id + ) # Check sub device 3 uses Bedroom sub_device_3 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_33333333")} ) assert sub_device_3 is not None - assert sub_device_3.suggested_area == "Bedroom" + assert sub_device_3.area_id == area_registry.async_get_area_by_name("Bedroom").id @patch("homeassistant.components.esphome.manager.secrets.token_bytes") diff --git a/tests/components/flo/snapshots/test_init.ambr b/tests/components/flo/snapshots/test_init.ambr index edba0ebe162..51e7bbd6dce 100644 --- a/tests/components/flo/snapshots/test_init.ambr +++ b/tests/components/flo/snapshots/test_init.ambr @@ -32,7 +32,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '6.1.1', 'via_device_id': None, }), @@ -67,7 +66,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111112', - 'suggested_area': None, 'sw_version': '1.1.15', 'via_device_id': None, }), diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr index d2af92b3f8f..c26d39a5e25 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_init.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.2.3', 'via_device_id': None, }) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index 0c57935589b..f11791b8ed1 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -59,7 +58,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -90,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -121,7 +118,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/homee/snapshots/test_init.ambr b/tests/components/homee/snapshots/test_init.ambr index 664740dbeac..dc56290e93e 100644 --- a/tests/components/homee/snapshots/test_init.ambr +++ b/tests/components/homee/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.2.3', 'via_device_id': None, }) @@ -64,7 +63,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.54', 'via_device_id': , }) diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 4540cfd239a..556be38f702 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -34,7 +34,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234', - 'suggested_area': None, 'sw_version': '0.8.16', }), 'entities': list([ @@ -665,7 +664,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000A', - 'suggested_area': None, 'sw_version': '2.1.6', }), 'entities': list([ @@ -747,7 +745,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000D', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -1005,7 +1002,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000B', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -1263,7 +1259,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000C', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -1525,7 +1520,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00aa00000a0', - 'suggested_area': None, 'sw_version': '3.3.0', }), 'entities': list([ @@ -1746,7 +1740,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '158d0007c59c6a', - 'suggested_area': None, 'sw_version': '0', }), 'entities': list([ @@ -1923,7 +1916,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0000000123456789', - 'suggested_area': None, 'sw_version': '1.4.7', }), 'entities': list([ @@ -2215,7 +2207,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '111a1111a1a111', - 'suggested_area': None, 'sw_version': '9', }), 'entities': list([ @@ -2349,7 +2340,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00A0000000000', - 'suggested_area': None, 'sw_version': '1.10.931', }), 'entities': list([ @@ -2863,7 +2853,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1020301376', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -3335,7 +3324,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -3510,7 +3498,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -3992,7 +3979,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4167,7 +4153,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4346,7 +4331,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4612,7 +4596,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4871,7 +4854,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5130,7 +5112,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5389,7 +5370,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5648,7 +5628,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5914,7 +5893,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6173,7 +6151,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6432,7 +6409,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6698,7 +6674,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6957,7 +6932,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '4.8.70226', }), 'entities': list([ @@ -7357,7 +7331,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -7623,7 +7596,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -7886,7 +7858,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -8372,7 +8343,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -8497,7 +8467,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -8798,7 +8767,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -8973,7 +8941,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -9152,7 +9119,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789016', - 'suggested_area': None, 'sw_version': '4.7.340214', }), 'entities': list([ @@ -9647,7 +9613,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '4.5.130201', }), 'entities': list([ @@ -9958,7 +9923,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', - 'suggested_area': None, 'sw_version': '1.2.8', }), 'entities': list([ @@ -10341,7 +10305,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', - 'suggested_area': None, 'sw_version': '1.2.9', }), 'entities': list([ @@ -10712,7 +10675,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-1', - 'suggested_area': None, 'sw_version': '5.0.18', }), 'entities': list([ @@ -10934,7 +10896,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-2', - 'suggested_area': None, 'sw_version': '5.0.18', }), 'entities': list([ @@ -11062,7 +11023,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -11236,7 +11196,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -11318,7 +11277,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -11496,7 +11454,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11629,7 +11586,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11711,7 +11667,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11849,7 +11804,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12197,7 +12151,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12283,7 +12236,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12365,7 +12317,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', - 'suggested_area': None, 'sw_version': '1.4.84', }), 'entities': list([ @@ -12551,7 +12502,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -12725,7 +12675,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12807,7 +12756,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -12985,7 +12933,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13118,7 +13065,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13200,7 +13146,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13339,7 +13284,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13421,7 +13365,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13560,7 +13503,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -13917,7 +13859,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14003,7 +13944,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14085,7 +14025,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14278,7 +14217,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14360,7 +14298,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14553,7 +14490,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14635,7 +14571,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', - 'suggested_area': None, 'sw_version': '1.4.84', }), 'entities': list([ @@ -14836,7 +14771,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00000001', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -15050,7 +14984,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462395276914', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15197,7 +15130,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462395276939', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15344,7 +15276,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462403113447', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15491,7 +15422,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462403233419', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15638,7 +15568,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462412411853', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15795,7 +15724,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462412413293', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15952,7 +15880,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462389072572', - 'suggested_area': None, 'sw_version': '45.1.17846', }), 'entities': list([ @@ -16286,7 +16213,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462378982941', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16420,7 +16346,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462378983942', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16554,7 +16479,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462379122122', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16688,7 +16612,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462379123707', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16822,7 +16745,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462383114163', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16956,7 +16878,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462383114193', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -17090,7 +17011,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462385996792', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -17224,7 +17144,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456', - 'suggested_area': None, 'sw_version': '1.32.1932126170', }), 'entities': list([ @@ -17310,7 +17229,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', - 'suggested_area': None, 'sw_version': '2.2.15', }), 'entities': list([ @@ -17463,7 +17381,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'EUCP03190xxxxx48', - 'suggested_area': None, 'sw_version': '2.3.7', }), 'entities': list([ @@ -17642,7 +17559,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'CNNT061751001372', - 'suggested_area': None, 'sw_version': '1.0.3', }), 'entities': list([ @@ -17862,7 +17778,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'XXXXXXXX', - 'suggested_area': None, 'sw_version': '3.40.XX', }), 'entities': list([ @@ -18162,7 +18077,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '999AAAAAA999', - 'suggested_area': None, 'sw_version': '04.71.04', }), 'entities': list([ @@ -18354,7 +18268,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '39024290', - 'suggested_area': None, 'sw_version': '001.005', }), 'entities': list([ @@ -18487,7 +18400,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '12344331', - 'suggested_area': None, 'sw_version': '08.08', }), 'entities': list([ @@ -18573,7 +18485,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'HH41234', - 'suggested_area': None, 'sw_version': '4.2.3', }), 'entities': list([ @@ -18869,7 +18780,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'BB1121', - 'suggested_area': None, 'sw_version': '4.1.9', }), 'entities': list([ @@ -19007,7 +18917,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', - 'suggested_area': None, 'sw_version': '2.8.1', }), 'entities': list([ @@ -19357,7 +19266,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', - 'suggested_area': None, 'sw_version': '1.4.40', }), 'entities': list([ @@ -19642,7 +19550,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'g738658', - 'suggested_area': None, 'sw_version': '80.0.0', }), 'entities': list([ @@ -19953,7 +19860,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234', - 'suggested_area': None, 'sw_version': '1.0.3', }), 'entities': list([ @@ -20125,7 +20031,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAAAAAAAA', - 'suggested_area': None, 'sw_version': '59', }), 'entities': list([ @@ -20451,7 +20356,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00aa0000aa0a', - 'suggested_area': None, 'sw_version': '1.0.4', }), 'entities': list([ @@ -20897,7 +20801,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21071,7 +20974,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0101.3521.0436', - 'suggested_area': None, 'sw_version': '1.3.0', }), 'entities': list([ @@ -21153,7 +21055,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '', - 'suggested_area': None, 'sw_version': '', }), 'entities': list([ @@ -21331,7 +21232,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21505,7 +21405,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21679,7 +21578,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21853,7 +21751,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0401.3521.0679', - 'suggested_area': None, 'sw_version': '1.3.0', }), 'entities': list([ @@ -21935,7 +21832,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -22113,7 +22009,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', - 'suggested_area': None, 'sw_version': '004.027.000', }), 'entities': list([ @@ -22242,7 +22137,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234567890abcd', - 'suggested_area': None, 'sw_version': '', }), 'entities': list([ @@ -22432,7 +22326,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '0.0.0', }), 'entities': list([ @@ -22563,7 +22456,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '3.3.0', }), 'entities': list([ @@ -22978,7 +22870,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '16.0.0', }), 'entities': list([ @@ -23208,7 +23099,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'a1a11a1', - 'suggested_area': None, 'sw_version': '70', }), 'entities': list([ @@ -23290,7 +23180,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'a11b111', - 'suggested_area': None, 'sw_version': '16', }), 'entities': list([ @@ -23516,7 +23405,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1111111a114a111a', - 'suggested_area': None, 'sw_version': '48', }), 'entities': list([ @@ -23647,7 +23535,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '0.0.0', }), 'entities': list([ @@ -23778,7 +23665,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '15.0.0', }), 'entities': list([ @@ -23908,7 +23794,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AM01121849000327', - 'suggested_area': None, 'sw_version': '3.121.2', }), 'entities': list([ @@ -24229,7 +24114,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'EU0121203xxxxx07', - 'suggested_area': None, 'sw_version': '1.101.2', }), 'entities': list([ diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 656978a08a2..166fd1a9e65 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -328,6 +328,8 @@ async def test_snapshots( device_dict.pop("created_at", None) device_dict.pop("modified_at", None) device_dict.pop("_cache", None) + # This can be removed when suggested_area is removed from DeviceEntry + device_dict.pop("_suggested_area") devices.append({"device": device_dict, "entities": entities}) diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index a07c0745c45..3b6264367e2 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -80,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index 3224a0cc63e..b75b89269f1 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -89,7 +89,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -184,7 +183,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_select.ambr b/tests/components/homewizard/snapshots/test_select.ambr index ecfd80e04da..dd331c3f49b 100644 --- a/tests/components/homewizard/snapshots/test_select.ambr +++ b/tests/components/homewizard/snapshots/test_select.ambr @@ -90,7 +90,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 9f95e140edc..f870170bae9 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -119,7 +118,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -212,7 +210,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -305,7 +302,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -398,7 +394,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -491,7 +486,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -584,7 +578,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -677,7 +670,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -763,7 +755,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -856,7 +847,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -945,7 +935,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -1030,7 +1019,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1123,7 +1111,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1216,7 +1203,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1309,7 +1295,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1402,7 +1387,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1495,7 +1479,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1588,7 +1571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1678,7 +1660,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1771,7 +1752,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1864,7 +1844,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1949,7 +1928,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2038,7 +2016,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2131,7 +2108,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2224,7 +2200,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2317,7 +2292,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2410,7 +2384,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2503,7 +2476,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2596,7 +2568,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2689,7 +2660,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2782,7 +2752,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2875,7 +2844,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2968,7 +2936,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3061,7 +3028,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3154,7 +3120,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3244,7 +3209,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3334,7 +3298,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3424,7 +3387,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3517,7 +3479,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3610,7 +3571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3703,7 +3663,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3796,7 +3755,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3889,7 +3847,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3982,7 +3939,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4075,7 +4031,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4168,7 +4123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4261,7 +4215,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4354,7 +4307,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4439,7 +4391,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4528,7 +4479,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4618,7 +4568,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4711,7 +4660,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4804,7 +4752,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4897,7 +4844,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4982,7 +4928,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5075,7 +5020,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5168,7 +5112,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5261,7 +5204,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5354,7 +5296,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5447,7 +5388,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5540,7 +5480,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5633,7 +5572,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5726,7 +5664,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5819,7 +5756,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5912,7 +5848,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6005,7 +5940,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6090,7 +6024,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6180,7 +6113,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6273,7 +6205,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6358,7 +6289,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6451,7 +6381,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6544,7 +6473,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6637,7 +6565,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6722,7 +6649,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6807,7 +6733,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6906,7 +6831,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6999,7 +6923,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7092,7 +7015,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7185,7 +7107,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7278,7 +7199,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7363,7 +7283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7448,7 +7367,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7533,7 +7451,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7618,7 +7535,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7703,7 +7619,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7788,7 +7703,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7877,7 +7791,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7962,7 +7875,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8047,7 +7959,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'gas_meter_G001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8136,7 +8047,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'heat_meter_H001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8225,7 +8135,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_IH001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8310,7 +8219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'warm_water_meter_WW001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8399,7 +8307,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'water_meter_W001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8492,7 +8399,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8582,7 +8488,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8675,7 +8580,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8768,7 +8672,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8861,7 +8764,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8946,7 +8848,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9039,7 +8940,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9132,7 +9032,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9225,7 +9124,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9318,7 +9216,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9411,7 +9308,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9504,7 +9400,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9597,7 +9492,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9690,7 +9584,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9783,7 +9676,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9876,7 +9768,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9969,7 +9860,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10054,7 +9944,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10144,7 +10033,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10237,7 +10125,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10322,7 +10209,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10415,7 +10301,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10508,7 +10393,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10601,7 +10485,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10686,7 +10569,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10771,7 +10653,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10870,7 +10751,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10963,7 +10843,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11056,7 +10935,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11149,7 +11027,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11242,7 +11119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11327,7 +11203,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11412,7 +11287,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11497,7 +11371,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11582,7 +11455,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11667,7 +11539,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11752,7 +11623,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11841,7 +11711,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11926,7 +11795,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12011,7 +11879,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12100,7 +11967,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12189,7 +12055,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12274,7 +12139,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12363,7 +12227,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12456,7 +12319,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12546,7 +12408,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12639,7 +12500,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12732,7 +12592,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12825,7 +12684,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12918,7 +12776,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13011,7 +12868,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13104,7 +12960,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13197,7 +13052,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13290,7 +13144,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13383,7 +13236,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13476,7 +13328,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13569,7 +13420,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13662,7 +13512,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13755,7 +13604,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13848,7 +13696,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13933,7 +13780,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14026,7 +13872,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14111,7 +13956,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14204,7 +14048,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14297,7 +14140,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14390,7 +14232,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14483,7 +14324,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14576,7 +14416,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14669,7 +14508,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14762,7 +14600,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14847,7 +14684,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14932,7 +14768,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15017,7 +14852,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15102,7 +14936,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15187,7 +15020,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15272,7 +15104,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15361,7 +15192,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15446,7 +15276,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15535,7 +15364,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15628,7 +15456,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15721,7 +15548,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15814,7 +15640,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15907,7 +15732,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15992,7 +15816,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -16081,7 +15904,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16174,7 +15996,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16267,7 +16088,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16360,7 +16180,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16453,7 +16272,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16546,7 +16364,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16639,7 +16456,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16729,7 +16545,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16822,7 +16637,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16915,7 +16729,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -17008,7 +16821,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -17093,7 +16905,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -17182,7 +16993,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17275,7 +17085,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17364,7 +17173,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17449,7 +17257,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17538,7 +17345,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17631,7 +17437,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17724,7 +17529,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17817,7 +17621,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17910,7 +17713,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18003,7 +17805,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18096,7 +17897,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18186,7 +17986,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18279,7 +18078,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18372,7 +18170,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18457,7 +18254,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18546,7 +18342,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18639,7 +18434,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18732,7 +18526,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18825,7 +18618,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18918,7 +18710,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19011,7 +18802,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19104,7 +18894,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19197,7 +18986,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19290,7 +19078,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19383,7 +19170,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19476,7 +19262,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19569,7 +19354,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19662,7 +19446,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19752,7 +19535,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19842,7 +19624,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19932,7 +19713,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20025,7 +19805,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20118,7 +19897,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20211,7 +19989,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20304,7 +20081,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20397,7 +20173,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20490,7 +20265,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20583,7 +20357,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20676,7 +20449,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20769,7 +20541,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20862,7 +20633,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20947,7 +20717,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index c4e67003b58..49916a59d9e 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -79,7 +79,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -164,7 +163,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -250,7 +248,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -335,7 +332,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -420,7 +416,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -506,7 +501,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -591,7 +585,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -676,7 +669,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -761,7 +753,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -846,7 +837,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -931,7 +921,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 807996f1093..5f287b1d8e3 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -11,7 +11,11 @@ from homeassistant.components.light import ColorMode from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.util import color as color_util from .conftest import create_config_entry @@ -776,6 +780,7 @@ def test_hs_color() -> None: async def test_group_features( hass: HomeAssistant, + area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, mock_bridge_v1: Mock, @@ -966,16 +971,22 @@ async def test_group_features( entry = entity_registry.async_get("light.hue_lamp_1") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area is None + assert device_entry.area_id is None entry = entity_registry.async_get("light.hue_lamp_2") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Living Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living Room").id + ) entry = entity_registry.async_get("light.hue_lamp_3") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Living Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living Room").id + ) entry = entity_registry.async_get("light.hue_lamp_4") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Dining Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Dining Room").id + ) diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index 1428a75d7b4..e0627ad9da8 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': 'Garden', 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr index b7aa14ef0bf..e2b8eeba811 100644 --- a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/iotty/snapshots/test_switch.ambr b/tests/components/iotty/snapshots/test_switch.ambr index 058a5d35cd0..04712dbf022 100644 --- a/tests/components/iotty/snapshots/test_switch.ambr +++ b/tests/components/iotty/snapshots/test_switch.ambr @@ -39,7 +39,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index 7329eec7f70..6a5f5371a9d 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ituran/snapshots/test_init.ambr b/tests/components/ituran/snapshots/test_init.ambr index b97aef6027b..456687407e2 100644 --- a/tests/components/ituran/snapshots/test_init.ambr +++ b/tests/components/ituran/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '12345678', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 9c9f31a2544..350ac169938 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -75,7 +75,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -108,7 +107,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -189,7 +187,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -222,7 +219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/lamarzocco/snapshots/test_init.ambr b/tests/components/lamarzocco/snapshots/test_init.ambr index 18b2fd0fbc3..f11057f8620 100644 --- a/tests/components/lamarzocco/snapshots/test_init.ambr +++ b/tests/components/lamarzocco/snapshots/test_init.ambr @@ -35,7 +35,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GS012345', - 'suggested_area': None, 'sw_version': 'v1.17', 'via_device_id': None, }) diff --git a/tests/components/lektrico/snapshots/test_init.ambr b/tests/components/lektrico/snapshots/test_init.ambr index 35183bf5d75..a935f5cfa14 100644 --- a/tests/components/lektrico/snapshots/test_init.ambr +++ b/tests/components/lektrico/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '500006', - 'suggested_area': None, 'sw_version': '1.44', 'via_device_id': None, }) diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index e2a35bcb1b1..1b09d742876 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -14,7 +14,11 @@ from homeassistant.components.lifx.const import CONF_SERIAL from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.zeroconf import ( ATTR_PROPERTIES_ID, @@ -585,6 +589,7 @@ async def test_refuse_relays(hass: HomeAssistant) -> None: async def test_suggested_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -624,4 +629,4 @@ async def test_suggested_area( entity = entity_registry.async_get(entity_id) device = device_registry.async_get(entity.device_id) - assert device.suggested_area == "My LIFX Group" + assert device.area_id == area_registry.async_get_area_by_name("My LIFX Group").id diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr index 46fb4c1d4e0..4d3e9d7aeab 100644 --- a/tests/components/mastodon/snapshots/test_init.ambr +++ b/tests/components/mastodon/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.4.0-nightly.2025-02-07', 'via_device_id': None, }) diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index aada173ffc3..e3a9e608911 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'v1.10.2', 'via_device_id': None, }) diff --git a/tests/components/meater/snapshots/test_init.ambr b/tests/components/meater/snapshots/test_init.ambr index 68e4ba32a4a..95335942de6 100644 --- a/tests/components/meater/snapshots/test_init.ambr +++ b/tests/components/meater/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/miele/snapshots/test_init.ambr b/tests/components/miele/snapshots/test_init.ambr index eee976ab09f..9feeeb6523b 100644 --- a/tests/components/miele/snapshots/test_init.ambr +++ b/tests/components/miele/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'Dummy_Appliance_1', - 'suggested_area': None, 'sw_version': '31.17', 'via_device_id': None, }) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 15e203eab06..fdaed0c323f 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -32,7 +32,11 @@ from homeassistant.const import ( ) from homeassistant.core import HassJobType, HomeAssistant from homeassistant.generated.mqtt import MQTT -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util @@ -1415,13 +1419,14 @@ async def help_test_entity_device_info_with_identifier( config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" - registry = dr.async_get(hass) + area_registry = ar.async_get(hass) + device_registry = dr.async_get(hass) data = json.dumps(config) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) + device = device_registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -1430,7 +1435,7 @@ async def help_test_entity_device_info_with_identifier( assert device.model_id == "XYZ001" assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" - assert device.suggested_area == "default_area" + assert device.area_id == area_registry.async_get_area_by_name("default_area").id assert device.configuration_url == "http://example.com" @@ -1450,13 +1455,14 @@ async def help_test_entity_device_info_with_connection( config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) config["unique_id"] = "veryunique" - registry = dr.async_get(hass) + area_registry = ar.async_get(hass) + device_registry = dr.async_get(hass) data = json.dumps(config) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device( + device = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} ) assert device is not None @@ -1467,7 +1473,7 @@ async def help_test_entity_device_info_with_connection( assert device.model_id == "XYZ001" assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" - assert device.suggested_area == "default_area" + assert device.area_id == area_registry.async_get_area_by_name("default_area").id assert device.configuration_url == "http://example.com" diff --git a/tests/components/myuplink/snapshots/test_init.ambr b/tests/components/myuplink/snapshots/test_init.ambr index 14be11c36ec..56fb26b4084 100644 --- a/tests/components/myuplink/snapshots/test_init.ambr +++ b/tests/components/myuplink/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10001', - 'suggested_area': None, 'sw_version': '9682R7A', 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10002', - 'suggested_area': None, 'sw_version': '9682R7B', 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10003', - 'suggested_area': None, 'sw_version': '9682R7C', 'via_device_id': None, }) diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index 35e7f7efc29..95fb1f9ed45 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Corridor', 'sw_version': None, 'via_device_id': None, }) @@ -159,7 +155,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -192,7 +187,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -225,7 +219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -258,7 +251,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -291,7 +283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -324,7 +315,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -357,7 +347,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -390,7 +379,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -423,7 +411,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -456,7 +443,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -489,7 +475,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -522,7 +507,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -555,7 +539,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -588,7 +571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -621,7 +603,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -654,7 +635,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -687,7 +667,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -720,7 +699,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -753,7 +731,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -786,7 +763,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -819,7 +795,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -852,7 +827,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -885,7 +859,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -918,7 +891,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -951,7 +923,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -984,7 +955,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1017,7 +987,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1050,7 +1019,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Bureau', 'sw_version': None, 'via_device_id': None, }) @@ -1083,7 +1051,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Livingroom', 'sw_version': None, 'via_device_id': None, }) @@ -1116,7 +1083,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Entrada', 'sw_version': None, 'via_device_id': None, }) @@ -1149,7 +1115,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Cocina', 'sw_version': None, 'via_device_id': None, }) @@ -1182,7 +1147,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1215,7 +1179,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1248,7 +1211,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1281,7 +1243,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index 2a806be8ae1..2980e3f35f0 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'FFFFFFFFFFFFF', - 'suggested_area': None, 'sw_version': 'EC25AFFDR07A09M4G', 'via_device_id': None, }) diff --git a/tests/components/nut/test_init.py b/tests/components/nut/test_init.py index 6f1fb94478d..18c038c17a0 100644 --- a/tests/components/nut/test_init.py +++ b/tests/components/nut/test_init.py @@ -17,7 +17,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import area_registry as ar, device_registry as dr from homeassistant.setup import async_setup_component from .util import _get_mock_nutclient, async_init_integration @@ -247,6 +247,7 @@ async def test_serial_number( async def test_device_location( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test for suggested location on device.""" @@ -269,7 +270,10 @@ async def test_device_location( ) assert device_entry is not None - assert device_entry.suggested_area == mock_device_location + assert ( + device_entry.area_id + == area_registry.async_get_area_by_name(mock_device_location).id + ) async def test_update_options(hass: HomeAssistant) -> None: diff --git a/tests/components/nyt_games/snapshots/test_init.ambr b/tests/components/nyt_games/snapshots/test_init.ambr index d9ce6f15a4d..5ca9a2d8df2 100644 --- a/tests/components/nyt_games/snapshots/test_init.ambr +++ b/tests/components/nyt_games/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ohme/snapshots/test_init.ambr b/tests/components/ohme/snapshots/test_init.ambr index ccf09f546cf..2e8304489d9 100644 --- a/tests/components/ohme/snapshots/test_init.ambr +++ b/tests/components/ohme/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'chargerid', - 'suggested_area': None, 'sw_version': 'v2.65', 'via_device_id': None, }) diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index 07e56a78fae..787551ad90e 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.7.1-stable', 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.7.1-stable', 'via_device_id': None, }) diff --git a/tests/components/onedrive/snapshots/test_init.ambr b/tests/components/onedrive/snapshots/test_init.ambr index 9b2ed7e4d94..2f9cfc1a038 100644 --- a/tests/components/onedrive/snapshots/test_init.ambr +++ b/tests/components/onedrive/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/onewire/snapshots/test_init.ambr b/tests/components/onewire/snapshots/test_init.ambr index 9b2a0e00a62..26ed15fc897 100644 --- a/tests/components/onewire/snapshots/test_init.ambr +++ b/tests/components/onewire/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': , }) @@ -159,7 +155,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -192,7 +187,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -225,7 +219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -258,7 +251,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -291,7 +283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -324,7 +315,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222222', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -357,7 +347,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222223', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -390,7 +379,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -423,7 +411,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -456,7 +443,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -489,7 +475,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -522,7 +507,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -555,7 +539,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -588,7 +571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222222', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -621,7 +603,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -654,7 +635,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -687,7 +667,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111112', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -720,7 +699,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111113', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) diff --git a/tests/components/openai_conversation/snapshots/test_init.ambr b/tests/components/openai_conversation/snapshots/test_init.ambr index 4eff869b016..0058416b254 100644 --- a/tests/components/openai_conversation/snapshots/test_init.ambr +++ b/tests/components/openai_conversation/snapshots/test_init.ambr @@ -21,7 +21,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -48,7 +47,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/overseerr/snapshots/test_init.ambr b/tests/components/overseerr/snapshots/test_init.ambr index 2709f532ef6..71c1b9ffd3a 100644 --- a/tests/components/overseerr/snapshots/test_init.ambr +++ b/tests/components/overseerr/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/palazzetti/snapshots/test_init.ambr b/tests/components/palazzetti/snapshots/test_init.ambr index fc96cab4fad..b69982d9c08 100644 --- a/tests/components/palazzetti/snapshots/test_init.ambr +++ b/tests/components/palazzetti/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.0.0', 'via_device_id': None, }) diff --git a/tests/components/peblar/snapshots/test_init.ambr b/tests/components/peblar/snapshots/test_init.ambr index 8a7cefc523d..97c0737e402 100644 --- a/tests/components/peblar/snapshots/test_init.ambr +++ b/tests/components/peblar/snapshots/test_init.ambr @@ -35,7 +35,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '23-45-A4O-MOF', - 'suggested_area': None, 'sw_version': '1.6.1+1+WL-1', 'via_device_id': None, }) diff --git a/tests/components/rainforest_raven/snapshots/test_init.ambr b/tests/components/rainforest_raven/snapshots/test_init.ambr index 8a143f9963f..f34d33d6c24 100644 --- a/tests/components/rainforest_raven/snapshots/test_init.ambr +++ b/tests/components/rainforest_raven/snapshots/test_init.ambr @@ -32,7 +32,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.0.0 (7400)', 'via_device_id': None, }), diff --git a/tests/components/renault/snapshots/test_init.ambr b/tests/components/renault/snapshots/test_init.ambr index 9a10083b227..defb0f249ff 100644 --- a/tests/components/renault/snapshots/test_init.ambr +++ b/tests/components/renault/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -63,7 +62,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -98,7 +96,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -133,7 +130,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -168,7 +164,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index c3aec4f0968..bc5022a7724 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -9,7 +9,11 @@ from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON from homeassistant.components.roku.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from . import UPNP_SERIAL @@ -77,12 +81,13 @@ async def test_roku_binary_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_rokutv_binary_sensors( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -158,4 +163,6 @@ async def test_rokutv_binary_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 7586e85b715..2607c79086a 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -60,7 +60,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.core_config import async_process_ha_core_config -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -100,7 +104,7 @@ async def test_setup( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/roku3-idle.json"], indirect=True) @@ -118,6 +122,7 @@ async def test_idle_setup( @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_tv_setup( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -146,7 +151,9 @@ async def test_tv_setup( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) @pytest.mark.parametrize( diff --git a/tests/components/roku/test_sensor.py b/tests/components/roku/test_sensor.py index e65424e3e66..72f57729cc4 100644 --- a/tests/components/roku/test_sensor.py +++ b/tests/components/roku/test_sensor.py @@ -12,7 +12,11 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from . import UPNP_SERIAL @@ -60,12 +64,13 @@ async def test_roku_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_rokutv_sensors( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -106,4 +111,6 @@ async def test_rokutv_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 8eb77006061..3715f994fb0 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/russound_rio/snapshots/test_init.ambr b/tests/components/russound_rio/snapshots/test_init.ambr index e3185a06b24..0fcebb8a6e5 100644 --- a/tests/components/russound_rio/snapshots/test_init.ambr +++ b/tests/components/russound_rio/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index b29b824a7dd..f9006c7fd52 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -32,7 +32,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -67,7 +66,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -106,7 +104,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/schlage/snapshots/test_init.ambr b/tests/components/schlage/snapshots/test_init.ambr index a7f94b80038..4e57ad5d5c6 100644 --- a/tests/components/schlage/snapshots/test_init.ambr +++ b/tests/components/schlage/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0', 'via_device_id': None, }) diff --git a/tests/components/sensibo/snapshots/test_entity.ambr b/tests/components/sensibo/snapshots/test_entity.ambr index 80ee847cb55..ee0b3835da4 100644 --- a/tests/components/sensibo/snapshots/test_entity.ambr +++ b/tests/components/sensibo/snapshots/test_entity.ambr @@ -32,7 +32,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': 'Hallway', 'sw_version': 'SKY30046', 'via_device_id': None, }), @@ -67,7 +66,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0987654321', - 'suggested_area': 'Kitchen', 'sw_version': 'PUR00111', 'via_device_id': None, }), @@ -102,7 +100,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0987654329', - 'suggested_area': 'Bedroom', 'sw_version': 'PUR00111', 'via_device_id': None, }), @@ -133,7 +130,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'V17', 'via_device_id': , }), diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 0ee34eebf3f..f0193b6ce1c 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), @@ -161,7 +160,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 39dd9e512ae..e3e5475ca34 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index cd762a4b2ea..681c3a84191 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/slide_local/snapshots/test_init.ambr b/tests/components/slide_local/snapshots/test_init.ambr index 5b1a9f5ce2f..e2dec748e2a 100644 --- a/tests/components/slide_local/snapshots/test_init.ambr +++ b/tests/components/slide_local/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890ab', - 'suggested_area': None, 'sw_version': 2, 'via_device_id': None, }) diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 6ce3992d2b4..d63ac4e9ab4 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -30,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -63,7 +62,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Toilet', 'sw_version': None, 'via_device_id': None, }) @@ -96,7 +94,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -129,7 +126,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -162,7 +158,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -195,7 +190,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -228,7 +222,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -261,7 +254,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -294,7 +286,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -327,7 +318,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'ASM-KR-TP1-22-ACMB1M_16240426', 'via_device_id': None, }) @@ -360,7 +350,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AEH-WW-TP1-22-AE6000_17240903', 'via_device_id': None, }) @@ -393,7 +382,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -426,7 +414,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'ARTIK051_PRAC_20K_11230313', 'via_device_id': None, }) @@ -459,7 +446,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'ARA-WW-TP1-22-COMMON_11240702', 'via_device_id': None, }) @@ -492,7 +478,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -525,7 +510,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -558,7 +542,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'AKS-WW-TP2-20-MICROWAVE-OTR_40230125', 'via_device_id': None, }) @@ -591,7 +574,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AKS-WW-TP1X-21-OVEN_40211229', 'via_device_id': None, }) @@ -624,7 +606,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AKS-WW-TP1-20-OVEN-3-CR_40240205', 'via_device_id': None, }) @@ -657,7 +638,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'A-RFWW-TP2-21-COMMON_20220110', 'via_device_id': None, }) @@ -690,7 +670,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20240616.213423', 'via_device_id': None, }) @@ -723,7 +702,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'A-RFWW-TP1-22-REV1_20241030', 'via_device_id': None, }) @@ -756,7 +734,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250123.105306', 'via_device_id': None, }) @@ -789,7 +766,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': '1.0', 'via_device_id': None, }) @@ -822,7 +798,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -855,7 +830,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -888,7 +862,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -921,7 +894,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_DW_A51_20_COMMON_30230714', 'via_device_id': None, }) @@ -954,7 +926,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_DF_TP2_20_COMMON_30230807', 'via_device_id': None, }) @@ -987,7 +958,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1020,7 +990,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1053,7 +1022,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_WM_TP2_20_COMMON_30230804', 'via_device_id': None, }) @@ -1086,7 +1054,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1119,7 +1086,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_TP1_21_COMMON_30240927', 'via_device_id': None, }) @@ -1152,7 +1118,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1185,7 +1150,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250206213001', 'via_device_id': None, }) @@ -1218,7 +1182,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250206151734', 'via_device_id': None, }) @@ -1251,7 +1214,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250308073247', 'via_device_id': None, }) @@ -1284,7 +1246,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1317,7 +1278,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1350,7 +1310,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1383,7 +1342,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1416,7 +1374,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1449,7 +1406,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1482,7 +1438,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1515,7 +1470,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.122.2', 'via_device_id': None, }) @@ -1548,7 +1502,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.122.2', 'via_device_id': None, }) @@ -1581,7 +1534,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'HW-Q80RWWB-1012.6', 'via_device_id': None, }) @@ -1614,7 +1566,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1647,7 +1598,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1680,7 +1630,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'V310XXU1AWK1', 'via_device_id': None, }) @@ -1713,7 +1662,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1746,7 +1694,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1779,7 +1726,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1812,7 +1758,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '6004971003', 'via_device_id': None, }) @@ -1845,7 +1790,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'SKY40147', 'via_device_id': None, }) @@ -1878,7 +1822,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1911,7 +1854,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1944,7 +1886,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.3.1 Build 240621 Rel.162048', 'via_device_id': None, }) @@ -1977,7 +1918,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'SAT-iMX8M23WWC-1010.5', 'via_device_id': None, }) @@ -2010,7 +1950,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'SAT-MT8532D24WWC-1016.0', 'via_device_id': None, }) @@ -2043,7 +1982,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'latest', 'via_device_id': None, }) @@ -2076,7 +2014,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'T-KTMAKUC-1290.3', 'via_device_id': None, }) @@ -2109,7 +2046,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2142,7 +2078,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2175,7 +2110,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2208,7 +2142,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2245,7 +2178,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': '000.055.00005', 'via_device_id': None, }) diff --git a/tests/components/smarty/snapshots/test_init.ambr b/tests/components/smarty/snapshots/test_init.ambr index a292cc97f47..ffa30051726 100644 --- a/tests/components/smarty/snapshots/test_init.ambr +++ b/tests/components/smarty/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 127, 'via_device_id': None, }) diff --git a/tests/components/smlight/snapshots/test_init.ambr b/tests/components/smlight/snapshots/test_init.ambr index ba374199254..8f533a42e36 100644 --- a/tests/components/smlight/snapshots/test_init.ambr +++ b/tests/components/smlight/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'core: v2.3.6 / zigbee: 20240314', 'via_device_id': None, }) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index b15d7698e05..84ad624cdc8 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -54,7 +54,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import area_registry as ar, entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, CONNECTION_UPNP, @@ -83,11 +83,15 @@ async def test_device_registry( assert reg_device.manufacturer == "Sonos" assert reg_device.name == "Zone A" # Default device provides battery info, area should not be suggested - assert reg_device.suggested_area is None + assert reg_device.area_id is None async def test_device_registry_not_portable( - hass: HomeAssistant, device_registry: DeviceRegistry, async_setup_sonos, soco + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: DeviceRegistry, + async_setup_sonos, + soco, ) -> None: """Test non-portable sonos device registered in the device registry to ensure area suggested.""" soco.get_battery_info.return_value = {} @@ -97,7 +101,7 @@ async def test_device_registry_not_portable( identifiers={("sonos", "RINCON_test")} ) assert reg_device is not None - assert reg_device.suggested_area == "Zone A" + assert reg_device.area_id == area_registry.async_get_area_by_name("Zone A").id async def test_entity_basic( diff --git a/tests/components/squeezebox/snapshots/test_init.ambr b/tests/components/squeezebox/snapshots/test_init.ambr index 3fc65be834a..39664f9ecf2 100644 --- a/tests/components/squeezebox/snapshots/test_init.ambr +++ b/tests/components/squeezebox/snapshots/test_init.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '', 'via_device_id': , }) @@ -72,7 +71,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '', 'via_device_id': , }) diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index 5d166018160..a5a591af94c 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -76,7 +76,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) @@ -158,7 +157,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 0e4bb4e4e41..627f05432d2 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -80,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': None, }) diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index a1a98b028e3..54c648ba21b 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -77,7 +77,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) @@ -160,7 +159,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index ffa2c5df7fd..acabe061420 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -89,7 +89,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': None, }) diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index 28b5ef7a7ed..cfaca7b81f3 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0000-0000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index a568a7dcd82..b7bf9e6bfa5 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -76,7 +76,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/tesla_fleet/snapshots/test_init.ambr b/tests/components/tesla_fleet/snapshots/test_init.ambr index c482d33de86..a669813a3a5 100644 --- a/tests/components/tesla_fleet/snapshots/test_init.ambr +++ b/tests/components/tesla_fleet/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123456', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'LRWXF7EK4KC700000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '234', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index f1011034d63..39fc8d04984 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123456', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'LRW3F7EK4NC700000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '234', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/tile/snapshots/test_init.ambr b/tests/components/tile/snapshots/test_init.ambr index ffdf6a6251a..0c3e1faf090 100644 --- a/tests/components/tile/snapshots/test_init.ambr +++ b/tests/components/tile/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '01.12.14.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index c8251bccd4f..c12f73bd737 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -430,7 +430,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr index 84cc8f73bf3..7d49d2aedbc 100644 --- a/tests/components/tplink/snapshots/test_button.ambr +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -612,7 +612,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_camera.ambr b/tests/components/tplink/snapshots/test_camera.ambr index f50c5d70362..a0282401e58 100644 --- a/tests/components/tplink/snapshots/test_camera.ambr +++ b/tests/components/tplink/snapshots/test_camera.ambr @@ -82,7 +82,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index df63291175a..4a38bdbbe59 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -92,7 +92,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': , }) diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index ad0321accef..eb42e2a7298 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -196,7 +196,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index 5ff1d9c5458..bc71313bf96 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr index 9fc5181c45d..6bcd24521e4 100644 --- a/tests/components/tplink/snapshots/test_select.ambr +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 5c22c2f7d83..f95390a8a57 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_siren.ambr b/tests/components/tplink/snapshots/test_siren.ambr index 761df4fcf21..7f90915f624 100644 --- a/tests/components/tplink/snapshots/test_siren.ambr +++ b/tests/components/tplink/snapshots/test_siren.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index 4b04587db05..98584c79759 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_vacuum.ambr b/tests/components/tplink/snapshots/test_vacuum.ambr index 68d14270b55..e5b28f5ac7a 100644 --- a/tests/components/tplink/snapshots/test_vacuum.ambr +++ b/tests/components/tplink/snapshots/test_vacuum.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 084e9a84401..fc30460bcc0 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 915c0f5080e..68f5a7b6adf 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -107,7 +107,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index 9e8bb6f7381..ad435a833ee 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -76,7 +76,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -158,7 +157,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -240,7 +238,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -322,7 +319,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -404,7 +400,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index 5c9ed6d4683..cb4563e0fb5 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -69,7 +69,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/velbus/snapshots/test_init.ambr b/tests/components/velbus/snapshots/test_init.ambr index 037ab7e6236..29f92126f95 100644 --- a/tests/components/velbus/snapshots/test_init.ambr +++ b/tests/components/velbus/snapshots/test_init.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }), @@ -59,7 +58,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }), @@ -90,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '2.0.0', 'via_device_id': None, }), @@ -121,7 +118,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6g7', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': , }), @@ -152,7 +148,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '12345', - 'suggested_area': None, 'sw_version': '1.0.1', 'via_device_id': , }), @@ -183,7 +178,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': , }), @@ -214,7 +208,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'asdfghjk', - 'suggested_area': None, 'sw_version': '3.0.0', 'via_device_id': , }), @@ -245,7 +238,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'qwerty1234567', - 'suggested_area': None, 'sw_version': '1.1.1', 'via_device_id': , }), @@ -276,7 +268,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'qwerty123', - 'suggested_area': None, 'sw_version': '1.0.1', 'via_device_id': , }), @@ -307,7 +298,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234', - 'suggested_area': None, 'sw_version': '1.0.1', 'via_device_id': , }), diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index fe330b82ca7..212535862f5 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -129,7 +128,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -229,7 +227,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -331,7 +328,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -433,7 +429,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -472,7 +467,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -511,7 +505,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -550,7 +543,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -589,7 +581,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -628,7 +619,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -734,7 +724,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -773,7 +762,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index 20bf56ef9c4..eac595cc0e9 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -67,7 +66,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -106,7 +104,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -145,7 +142,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -184,7 +180,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -277,7 +272,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -372,7 +366,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -411,7 +404,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -450,7 +442,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -489,7 +480,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -528,7 +518,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -636,7 +625,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index a47de22f68b..6aa25e0763a 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -163,7 +162,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -252,7 +250,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -438,7 +435,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -624,7 +620,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -663,7 +658,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -702,7 +696,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -792,7 +785,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -882,7 +874,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1245,7 +1236,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1284,7 +1274,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1323,7 +1312,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index edd2eee8b1f..8947ac40424 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -28,7 +28,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -113,7 +112,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -198,7 +196,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -283,7 +280,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -368,7 +364,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -407,7 +402,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -446,7 +440,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -531,7 +524,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -616,7 +608,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -702,7 +693,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -787,7 +777,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -826,7 +815,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/webostv/snapshots/test_media_player.ambr b/tests/components/webostv/snapshots/test_media_player.ambr index 9c097b166ec..d0a1142618a 100644 --- a/tests/components/webostv/snapshots/test_media_player.ambr +++ b/tests/components/webostv/snapshots/test_media_player.ambr @@ -63,7 +63,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': 'major.minor', 'via_device_id': None, }) diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 67f6baf45bb..38f125ad712 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -75,7 +75,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -157,7 +156,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -243,7 +241,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -325,7 +322,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -407,7 +403,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -488,7 +483,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -569,7 +563,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -650,7 +643,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -731,7 +723,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -864,7 +855,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/withings/snapshots/test_init.ambr b/tests/components/withings/snapshots/test_init.ambr index ec711def829..88d9ff94b1d 100644 --- a/tests/components/withings/snapshots/test_init.ambr +++ b/tests/components/withings/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index d8a29ed7c48..26f8817fa06 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -80,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index 877c8baa93e..5503b9a733d 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -88,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -182,7 +181,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 6cfbe1de5d4..dc8a2f09445 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -90,7 +90,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -322,7 +321,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -416,7 +414,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.99.0b1', 'via_device_id': None, }) @@ -510,7 +507,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.99.0b1', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index c32bc314cc0..09c86d81d44 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -81,7 +81,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -166,7 +165,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -252,7 +250,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -338,7 +335,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wmspro/snapshots/test_cover.ambr b/tests/components/wmspro/snapshots/test_cover.ambr index 53b2f6205cb..026785c9e1c 100644 --- a/tests/components/wmspro/snapshots/test_cover.ambr +++ b/tests/components/wmspro/snapshots/test_cover.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_init.ambr b/tests/components/wmspro/snapshots/test_init.ambr index 147d66f2b69..9d60cf8c907 100644 --- a/tests/components/wmspro/snapshots/test_init.ambr +++ b/tests/components/wmspro/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '19239', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -93,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -126,7 +123,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '19239', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -159,7 +155,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -192,7 +187,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -225,7 +219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '116682', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -258,7 +251,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '172555', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -291,7 +283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '18894', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -324,7 +315,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '230952', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -357,7 +347,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '284942', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -390,7 +379,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '328518', - 'suggested_area': 'Alle', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_light.ambr b/tests/components/wmspro/snapshots/test_light.ambr index d6ccebfb5ea..694fb2d51e4 100644 --- a/tests/components/wmspro/snapshots/test_light.ambr +++ b/tests/components/wmspro/snapshots/test_light.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_scene.ambr b/tests/components/wmspro/snapshots/test_scene.ambr index b5dddb368c9..97f47dc7f15 100644 --- a/tests/components/wmspro/snapshots/test_scene.ambr +++ b/tests/components/wmspro/snapshots/test_scene.ambr @@ -41,7 +41,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '42581', - 'suggested_area': 'Raum 0', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wolflink/snapshots/test_sensor.ambr b/tests/components/wolflink/snapshots/test_sensor.ambr index c5b23cc8e79..3d6e3fea3b5 100644 --- a/tests/components/wolflink/snapshots/test_sensor.ambr +++ b/tests/components/wolflink/snapshots/test_sensor.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/wyoming/test_devices.py b/tests/components/wyoming/test_devices.py index 24423264f93..d03f2622c71 100644 --- a/tests/components/wyoming/test_devices.py +++ b/tests/components/wyoming/test_devices.py @@ -8,13 +8,14 @@ from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import area_registry as ar, device_registry as dr async def test_device_registry_info( hass: HomeAssistant, satellite_device: SatelliteDevice, satellite_config_entry: ConfigEntry, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test info in device registry.""" @@ -26,7 +27,7 @@ async def test_device_registry_info( ) assert device is not None assert device.name == "Test Satellite" - assert device.suggested_area == "Office" + assert device.area_id == area_registry.async_get_area_by_name("Office").id # Check associated entities assist_in_progress_id = satellite_device.get_assist_in_progress_entity_id(hass) diff --git a/tests/components/yale/snapshots/test_binary_sensor.ambr b/tests/components/yale/snapshots/test_binary_sensor.ambr index 9db0d760efb..df0e604c550 100644 --- a/tests/components/yale/snapshots/test_binary_sensor.ambr +++ b/tests/components/yale/snapshots/test_binary_sensor.ambr @@ -27,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'tmt100 Name', 'sw_version': '3.1.0-HYDRC75+201909251139', 'via_device_id': None, }) diff --git a/tests/components/yale/snapshots/test_lock.ambr b/tests/components/yale/snapshots/test_lock.ambr index 00653a9b0c1..dd2faa8b69e 100644 --- a/tests/components/yale/snapshots/test_lock.ambr +++ b/tests/components/yale/snapshots/test_lock.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'online_with_doorsense Name', 'sw_version': 'undefined-4.3.0-1.8.14', 'via_device_id': None, }) diff --git a/tests/helpers/snapshots/test_entity_platform.ambr b/tests/helpers/snapshots/test_entity_platform.ambr index 55ff772e08e..ce0abffd03c 100644 --- a/tests/helpers/snapshots/test_entity_platform.ambr +++ b/tests/helpers/snapshots/test_entity_platform.ambr @@ -31,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Heliport', 'sw_version': 'test-sw', 'via_device_id': , }) @@ -68,7 +67,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Heliport', 'sw_version': 'test-sw', 'via_device_id': , }) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index a66684c94e3..4247da296fd 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -107,7 +107,6 @@ async def test_get_or_create_returns_same_entry( assert entry3.model == "model" assert entry3.name == "name" assert entry3.sw_version == "sw-version" - assert entry3.suggested_area == "Game Room" assert entry3.area_id == game_room_area.id await hass.async_block_till_done() @@ -409,7 +408,6 @@ async def test_loading_from_storage( name="name", primary_config_entry=mock_config_entry.entry_id, serial_number="serial_no", - suggested_area=None, # Not stored sw_version="version", ) assert isinstance(entry.config_entries, set) @@ -2509,13 +2507,13 @@ async def test_loading_saving_data( # Ensure a save/load cycle does not keep suggested area new_kitchen_light = registry2.async_get_device(identifiers={("hue", "999")}) - assert orig_kitchen_light.suggested_area == "Kitchen" + assert orig_kitchen_light.area_id == "kitchen" - orig_kitchen_light_witout_suggested_area = device_registry.async_update_device( + orig_kitchen_light_without_suggested_area = device_registry.async_update_device( orig_kitchen_light.id, suggested_area=None ) - assert orig_kitchen_light_witout_suggested_area.suggested_area is None - assert orig_kitchen_light_witout_suggested_area == new_kitchen_light + assert orig_kitchen_light_without_suggested_area.area_id == "kitchen" + assert orig_kitchen_light_without_suggested_area == new_kitchen_light async def test_no_unnecessary_changes( @@ -3225,7 +3223,6 @@ async def test_update_suggested_area( connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bla", "123")}, ) - assert not entry.suggested_area assert entry.area_id is None suggested_area = "Pool" @@ -3237,7 +3234,6 @@ async def test_update_suggested_area( assert mock_save.call_count == 1 assert updated_entry != entry - assert updated_entry.suggested_area == suggested_area pool_area = area_registry.async_get_area_by_name("Pool") assert pool_area is not None @@ -3267,7 +3263,7 @@ async def test_update_suggested_area( assert len(update_events) == 2 assert mock_save_2.call_count == 0 assert updated_entry != entry - assert updated_entry.suggested_area == "Other" + assert updated_entry.area_id == pool_area.id async def test_cleanup_device_registry( @@ -3475,7 +3471,6 @@ async def test_restore_device( name=None, primary_config_entry=entry_id, serial_number=None, - suggested_area=None, sw_version=None, ) # This will restore the original device, user customizations of @@ -4905,3 +4900,36 @@ async def test_connections_validator() -> None: """Test checking connections validator.""" with pytest.raises(ValueError, match="Invalid mac address format"): dr.DeviceEntry(connections={(dr.CONNECTION_NETWORK_MAC, "123456ABCDEF")}) + + +async def test_suggested_area_deprecation( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + area_registry: ar.AreaRegistry, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Make sure we do not duplicate entries.""" + entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="name", + manufacturer="manufacturer", + model="model", + suggested_area="Game Room", + ) + + game_room_area = area_registry.async_get_area_by_name("Game Room") + assert game_room_area is not None + assert len(area_registry.areas) == 1 + + assert len(device_registry.devices) == 1 + assert entry.area_id == game_room_area.id + assert entry.suggested_area == "Game Room" + + assert ( + "The deprecated function suggested_area was called. It will be removed in " + "HA Core 2026.9. Use code which ignores suggested_area instead" + ) in caplog.text diff --git a/tests/syrupy.py b/tests/syrupy.py index e028d5839cb..642e5a519b2 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -173,6 +173,8 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): if serialized["primary_config_entry"] is not None: serialized["primary_config_entry"] = ANY serialized.pop("_cache") + # This can be removed when suggested_area is removed from DeviceEntry + serialized.pop("_suggested_area") return cls._remove_created_and_modified_at(serialized) @classmethod From 924a86dfb609b9f87ce0ca11ea9b1d3182154e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 1 Aug 2025 09:51:48 +0100 Subject: [PATCH 183/265] Add nameservers to supervisor system health response (#149749) --- homeassistant/components/hassio/strings.json | 1 + homeassistant/components/hassio/system_health.py | 10 ++++++++++ tests/components/hassio/test_system_health.py | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 6d67b4b79c0..1e312ee34d9 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -9,6 +9,7 @@ "healthy": "Healthy", "host_os": "Host operating system", "installed_addons": "Installed add-ons", + "nameservers": "Nameservers", "supervisor_api": "Supervisor API", "supervisor_version": "Supervisor version", "supported": "Supported", diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index bc8da2a2a92..0a7e9b51e97 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -54,6 +54,15 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: "error": "Unsupported", } + nameservers = set() + for interface in network_info.get("interfaces", []): + if not interface.get("primary"): + continue + if ipv4 := interface.get("ipv4"): + nameservers.update(ipv4.get("nameservers", [])) + if ipv6 := interface.get("ipv6"): + nameservers.update(ipv6.get("nameservers", [])) + information = { "host_os": host_info.get("operating_system"), "update_channel": info.get("channel"), @@ -62,6 +71,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: "docker_version": info.get("docker"), "disk_total": f"{host_info.get('disk_total')} GB", "disk_used": f"{host_info.get('disk_used')} GB", + "nameservers": ", ".join(nameservers), "healthy": healthy, "supported": supported, "host_connectivity": network_info.get("host_internet"), diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index c4c2b861e6e..4839486810a 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -55,6 +55,10 @@ async def test_hassio_system_health( hass.data["hassio_network_info"] = { "host_internet": True, "supervisor_internet": True, + "interfaces": [ + {"primary": False, "ipv4": {"nameservers": ["9.9.9.9"]}}, + {"primary": True, "ipv4": {"nameservers": ["1.1.1.1"]}}, + ], } with patch.dict(os.environ, MOCK_ENVIRON): @@ -76,6 +80,7 @@ async def test_hassio_system_health( "host_os": "Home Assistant OS 5.9", "installed_addons": "Awesome Addon (1.0.0)", "ntp_synchronized": True, + "nameservers": "1.1.1.1", "supervisor_api": "ok", "supervisor_version": "supervisor-2020.11.1", "supported": True, From d77a3fca83a723a3d346a345823599748604b7e8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Aug 2025 11:01:26 +0200 Subject: [PATCH 184/265] Exclude is_new from DeviceEntry snapshots (#149801) --- .../components/enphase_envoy/diagnostics.py | 1 + .../components/acaia/snapshots/test_init.ambr | 1 - .../airgradient/snapshots/test_init.ambr | 2 - .../alexa_devices/snapshots/test_init.ambr | 1 - .../aosmith/snapshots/test_device.ambr | 1 - .../apcupsd/snapshots/test_init.ambr | 4 - .../august/snapshots/test_binary_sensor.ambr | 1 - .../august/snapshots/test_lock.ambr | 1 - tests/components/axis/snapshots/test_hub.ambr | 2 - .../cambridge_audio/snapshots/test_init.ambr | 1 - .../components/deconz/snapshots/test_hub.ambr | 1 - .../snapshots/test_init.ambr | 3 - .../ecovacs/snapshots/test_init.ambr | 1 - .../elgato/snapshots/test_button.ambr | 2 - .../elgato/snapshots/test_light.ambr | 3 - .../elgato/snapshots/test_sensor.ambr | 5 - .../elgato/snapshots/test_switch.ambr | 2 - .../snapshots/test_diagnostics.ambr | 8 - tests/components/flo/snapshots/test_init.ambr | 2 - .../snapshots/test_init.ambr | 1 - .../snapshots/test_init.ambr | 4 - .../components/homee/snapshots/test_init.ambr | 2 - .../snapshots/test_init.ambr | 116 --------- .../homekit_controller/test_init.py | 1 + .../homewizard/snapshots/test_button.ambr | 1 - .../homewizard/snapshots/test_number.ambr | 2 - .../homewizard/snapshots/test_select.ambr | 1 - .../homewizard/snapshots/test_sensor.ambr | 231 ------------------ .../homewizard/snapshots/test_switch.ambr | 11 - .../snapshots/test_init.ambr | 1 - .../snapshots/test_init.ambr | 1 - .../iotty/snapshots/test_switch.ambr | 1 - .../ista_ecotrend/snapshots/test_init.ambr | 2 - .../ituran/snapshots/test_init.ambr | 1 - .../kitchen_sink/snapshots/test_switch.ambr | 4 - .../lamarzocco/snapshots/test_init.ambr | 1 - .../lektrico/snapshots/test_init.ambr | 1 - .../mastodon/snapshots/test_init.ambr | 1 - .../mealie/snapshots/test_init.ambr | 1 - .../meater/snapshots/test_init.ambr | 1 - .../components/miele/snapshots/test_init.ambr | 1 - .../myuplink/snapshots/test_init.ambr | 3 - .../netatmo/snapshots/test_init.ambr | 39 --- .../netgear_lte/snapshots/test_init.ambr | 1 - .../nyt_games/snapshots/test_init.ambr | 3 - .../components/ohme/snapshots/test_init.ambr | 1 - .../ondilo_ico/snapshots/test_init.ambr | 2 - .../onedrive/snapshots/test_init.ambr | 1 - .../onewire/snapshots/test_init.ambr | 22 -- .../snapshots/test_init.ambr | 2 - .../overseerr/snapshots/test_init.ambr | 1 - .../palazzetti/snapshots/test_init.ambr | 1 - .../peblar/snapshots/test_init.ambr | 1 - .../rainforest_raven/snapshots/test_init.ambr | 1 - .../renault/snapshots/test_init.ambr | 5 - .../components/rova/snapshots/test_init.ambr | 1 - .../russound_rio/snapshots/test_init.ambr | 1 - .../samsungtv/snapshots/test_init.ambr | 3 - .../schlage/snapshots/test_init.ambr | 1 - .../sensibo/snapshots/test_entity.ambr | 4 - .../sfr_box/snapshots/test_binary_sensor.ambr | 2 - .../sfr_box/snapshots/test_button.ambr | 1 - .../sfr_box/snapshots/test_sensor.ambr | 1 - .../slide_local/snapshots/test_init.ambr | 1 - .../smartthings/snapshots/test_init.ambr | 68 ------ .../smarty/snapshots/test_init.ambr | 1 - .../smlight/snapshots/test_init.ambr | 1 - .../squeezebox/snapshots/test_init.ambr | 2 - .../snapshots/test_binary_sensor.ambr | 2 - .../tailwind/snapshots/test_button.ambr | 1 - .../tailwind/snapshots/test_cover.ambr | 2 - .../tailwind/snapshots/test_number.ambr | 1 - .../components/tedee/snapshots/test_init.ambr | 2 - .../components/tedee/snapshots/test_lock.ambr | 1 - .../tesla_fleet/snapshots/test_init.ambr | 4 - .../teslemetry/snapshots/test_init.ambr | 4 - .../components/tile/snapshots/test_init.ambr | 1 - .../tplink/snapshots/test_binary_sensor.ambr | 1 - .../tplink/snapshots/test_button.ambr | 1 - .../tplink/snapshots/test_camera.ambr | 1 - .../tplink/snapshots/test_climate.ambr | 1 - .../components/tplink/snapshots/test_fan.ambr | 1 - .../tplink/snapshots/test_number.ambr | 1 - .../tplink/snapshots/test_select.ambr | 1 - .../tplink/snapshots/test_sensor.ambr | 1 - .../tplink/snapshots/test_siren.ambr | 1 - .../tplink/snapshots/test_switch.ambr | 1 - .../tplink/snapshots/test_vacuum.ambr | 1 - .../components/tuya/snapshots/test_init.ambr | 1 - .../twentemilieu/snapshots/test_calendar.ambr | 1 - .../twentemilieu/snapshots/test_sensor.ambr | 5 - .../uptime/snapshots/test_sensor.ambr | 1 - .../velbus/snapshots/test_init.ambr | 10 - .../components/vesync/snapshots/test_fan.ambr | 12 - .../vesync/snapshots/test_light.ambr | 12 - .../vesync/snapshots/test_sensor.ambr | 12 - .../vesync/snapshots/test_switch.ambr | 12 - .../webostv/snapshots/test_media_player.ambr | 1 - .../whois/snapshots/test_sensor.ambr | 10 - .../withings/snapshots/test_init.ambr | 2 - .../wled/snapshots/test_button.ambr | 1 - .../wled/snapshots/test_number.ambr | 2 - .../wled/snapshots/test_select.ambr | 4 - .../wled/snapshots/test_switch.ambr | 4 - .../wmspro/snapshots/test_cover.ambr | 1 - .../wmspro/snapshots/test_init.ambr | 12 - .../wmspro/snapshots/test_light.ambr | 1 - .../wmspro/snapshots/test_scene.ambr | 1 - .../wolflink/snapshots/test_sensor.ambr | 1 - .../yale/snapshots/test_binary_sensor.ambr | 1 - .../components/yale/snapshots/test_lock.ambr | 1 - .../snapshots/test_entity_platform.ambr | 2 - tests/syrupy.py | 1 + 113 files changed, 3 insertions(+), 735 deletions(-) diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index 6487830675f..93244068feb 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -118,6 +118,7 @@ async def async_get_config_entry_diagnostics( device_dict.pop("_cache", None) # This can be removed when suggested_area is removed from DeviceEntry device_dict.pop("_suggested_area") + device_dict.pop("is_new", None) device_entities.append({"device": device_dict, "entities": entities}) # remove envoy serial diff --git a/tests/components/acaia/snapshots/test_init.ambr b/tests/components/acaia/snapshots/test_init.ambr index d518de056b2..9e311260693 100644 --- a/tests/components/acaia/snapshots/test_init.ambr +++ b/tests/components/acaia/snapshots/test_init.ambr @@ -21,7 +21,6 @@ 'aa:bb:cc:dd:ee:ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Acaia', diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 96ce43260aa..2a1e3dcc7fd 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '84fce612f5b8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'AirGradient', @@ -57,7 +56,6 @@ '84fce612f5b8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'AirGradient', diff --git a/tests/components/alexa_devices/snapshots/test_init.ambr b/tests/components/alexa_devices/snapshots/test_init.ambr index c396c65246a..bf28f8fb1a1 100644 --- a/tests/components/alexa_devices/snapshots/test_init.ambr +++ b/tests/components/alexa_devices/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'echo_test_serial_number', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Amazon', diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index f814106870b..c4c1b0b1b93 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -17,7 +17,6 @@ 'junctionId', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'A. O. Smith', diff --git a/tests/components/apcupsd/snapshots/test_init.ambr b/tests/components/apcupsd/snapshots/test_init.ambr index 6ca412f7e34..414c3e451fd 100644 --- a/tests/components/apcupsd/snapshots/test_init.ambr +++ b/tests/components/apcupsd/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'XXXXXXXXXXXX', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'APC', @@ -49,7 +48,6 @@ 'XXXX', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'APC', @@ -81,7 +79,6 @@ 'mocked-config-entry-id', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'APC', @@ -113,7 +110,6 @@ 'mocked-config-entry-id', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'APC', diff --git a/tests/components/august/snapshots/test_binary_sensor.ambr b/tests/components/august/snapshots/test_binary_sensor.ambr index c9a7b7ba039..9d94ae9ffdc 100644 --- a/tests/components/august/snapshots/test_binary_sensor.ambr +++ b/tests/components/august/snapshots/test_binary_sensor.ambr @@ -17,7 +17,6 @@ 'tmt100', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'August Home Inc.', diff --git a/tests/components/august/snapshots/test_lock.ambr b/tests/components/august/snapshots/test_lock.ambr index eb2cf7a815a..8af45cae68c 100644 --- a/tests/components/august/snapshots/test_lock.ambr +++ b/tests/components/august/snapshots/test_lock.ambr @@ -21,7 +21,6 @@ 'online_with_doorsense', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'August Home Inc.', diff --git a/tests/components/axis/snapshots/test_hub.ambr b/tests/components/axis/snapshots/test_hub.ambr index ab4745011dd..663c52dd36c 100644 --- a/tests/components/axis/snapshots/test_hub.ambr +++ b/tests/components/axis/snapshots/test_hub.ambr @@ -21,7 +21,6 @@ '00:40:8c:12:34:56', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Axis Communications AB', @@ -57,7 +56,6 @@ '00:40:8c:12:34:56', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Axis Communications AB', diff --git a/tests/components/cambridge_audio/snapshots/test_init.ambr b/tests/components/cambridge_audio/snapshots/test_init.ambr index 71a54cdb001..22642635375 100644 --- a/tests/components/cambridge_audio/snapshots/test_init.ambr +++ b/tests/components/cambridge_audio/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '0020c2d8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Cambridge Audio', diff --git a/tests/components/deconz/snapshots/test_hub.ambr b/tests/components/deconz/snapshots/test_hub.ambr index b171dafbd5d..59e77c4fb12 100644 --- a/tests/components/deconz/snapshots/test_hub.ambr +++ b/tests/components/deconz/snapshots/test_hub.ambr @@ -17,7 +17,6 @@ '01234E56789A', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Dresden Elektronik', diff --git a/tests/components/devolo_home_network/snapshots/test_init.ambr b/tests/components/devolo_home_network/snapshots/test_init.ambr index 4f965ce8d05..13603beb8b4 100644 --- a/tests/components/devolo_home_network/snapshots/test_init.ambr +++ b/tests/components/devolo_home_network/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '1234567890', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'devolo', @@ -57,7 +56,6 @@ '1234567890', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'devolo', @@ -89,7 +87,6 @@ '1234567890', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'devolo', diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index 642f0db6813..0e847da73ad 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'E1234567890000000001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ecovacs', diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index 5ff3710dfd7..85f9fadd2a0 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -70,7 +70,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -155,7 +154,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index 8ee893f6be5..5dbc21f62df 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -102,7 +102,6 @@ 'CN11A1A00001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -221,7 +220,6 @@ 'CN11A1A00001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -340,7 +338,6 @@ 'CN11A1A00001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index ebf98ff02ae..f53f8d223bd 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -77,7 +77,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -172,7 +171,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -267,7 +265,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -359,7 +356,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -454,7 +450,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index 8c75ed137b1..61235f17ece 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -69,7 +69,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -153,7 +152,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index be638168b34..ca6c502d3be 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -50,7 +50,6 @@ '<>', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -297,7 +296,6 @@ '1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -927,7 +925,6 @@ '<>', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -1174,7 +1171,6 @@ '1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -1848,7 +1844,6 @@ '<>', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -2095,7 +2090,6 @@ '1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -2790,7 +2784,6 @@ '1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -3339,7 +3332,6 @@ '<>', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', diff --git a/tests/components/flo/snapshots/test_init.ambr b/tests/components/flo/snapshots/test_init.ambr index 51e7bbd6dce..6a242c4d2ce 100644 --- a/tests/components/flo/snapshots/test_init.ambr +++ b/tests/components/flo/snapshots/test_init.ambr @@ -22,7 +22,6 @@ '98765', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Flo by Moen', @@ -56,7 +55,6 @@ '32839', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Flo by Moen', diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr index c26d39a5e25..e11d42d970e 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_init.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '00000000-0000-0000-0000-000000000001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index f11791b8ed1..d0b92a7e88d 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -18,7 +18,6 @@ 'ulid-conversation', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Google', @@ -48,7 +47,6 @@ 'ulid-stt', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Google', @@ -78,7 +76,6 @@ 'ulid-ai-task', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Google', @@ -108,7 +105,6 @@ 'ulid-tts', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Google', diff --git a/tests/components/homee/snapshots/test_init.ambr b/tests/components/homee/snapshots/test_init.ambr index dc56290e93e..8f20bb10454 100644 --- a/tests/components/homee/snapshots/test_init.ambr +++ b/tests/components/homee/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '00055511EECC', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'homee', @@ -53,7 +52,6 @@ '00055511EECC-3', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 556be38f702..3b075b44356 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -24,7 +24,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Sleekpoint Innovations', @@ -654,7 +653,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Anker', @@ -735,7 +733,6 @@ '00:00:00:00:00:00:aid:4', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Anker', @@ -992,7 +989,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Anker', @@ -1249,7 +1245,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Anker', @@ -1510,7 +1505,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Aqara', @@ -1730,7 +1724,6 @@ '00:00:00:00:00:00:aid:33', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Aqara', @@ -1906,7 +1899,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Aqara', @@ -2197,7 +2189,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Aqara', @@ -2330,7 +2321,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netgear, Inc', @@ -2843,7 +2833,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ConnectSense', @@ -3314,7 +3303,6 @@ '00:00:00:00:00:00:aid:4', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -3488,7 +3476,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -3969,7 +3956,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -4143,7 +4129,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -4321,7 +4306,6 @@ '00:00:00:00:00:00:aid:4295608960', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -4586,7 +4570,6 @@ '00:00:00:00:00:00:aid:4298360914', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -4844,7 +4827,6 @@ '00:00:00:00:00:00:aid:4298360921', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -5102,7 +5084,6 @@ '00:00:00:00:00:00:aid:4298527970', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -5360,7 +5341,6 @@ '00:00:00:00:00:00:aid:4298527962', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -5618,7 +5598,6 @@ '00:00:00:00:00:00:aid:4295016858', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -5883,7 +5862,6 @@ '00:00:00:00:00:00:aid:4298360712', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -6141,7 +6119,6 @@ '00:00:00:00:00:00:aid:4298649931', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -6399,7 +6376,6 @@ '00:00:00:00:00:00:aid:4295608971', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -6664,7 +6640,6 @@ '00:00:00:00:00:00:aid:4298584118', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -6922,7 +6897,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -7321,7 +7295,6 @@ '00:00:00:00:00:00:aid:4295016969', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -7586,7 +7559,6 @@ '00:00:00:00:00:00:aid:4298568508', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -7848,7 +7820,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -8333,7 +8304,6 @@ '00:00:00:00:00:00:aid:4', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -8457,7 +8427,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -8757,7 +8726,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -8931,7 +8899,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -9109,7 +9076,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -9603,7 +9569,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -9913,7 +9878,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Elgato', @@ -10295,7 +10259,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Elgato', @@ -10665,7 +10628,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'José A. Jiménez Campos', @@ -10886,7 +10848,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'José A. Jiménez Campos', @@ -11013,7 +10974,6 @@ '00:00:00:00:00:00:aid:123016423', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -11186,7 +11146,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -11267,7 +11226,6 @@ '00:00:00:00:00:00:aid:878448248', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -11444,7 +11402,6 @@ '00:00:00:00:00:00:aid:766313939', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -11576,7 +11533,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -11657,7 +11613,6 @@ '00:00:00:00:00:00:aid:1256851357', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -11794,7 +11749,6 @@ '00:00:00:00:00:00:aid:1233851541', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lookin', @@ -12141,7 +12095,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -12226,7 +12179,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -12307,7 +12259,6 @@ '00:00:00:00:00:00:aid:3982136094', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'FirstAlert', @@ -12492,7 +12443,6 @@ '00:00:00:00:00:00:aid:123016423', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -12665,7 +12615,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -12746,7 +12695,6 @@ '00:00:00:00:00:00:aid:878448248', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -12923,7 +12871,6 @@ '00:00:00:00:00:00:aid:766313939', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13055,7 +13002,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13136,7 +13082,6 @@ '00:00:00:00:00:00:aid:1256851357', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13274,7 +13219,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13355,7 +13299,6 @@ '00:00:00:00:00:00:aid:1256851357', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13493,7 +13436,6 @@ '00:00:00:00:00:00:aid:1233851541', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lookin', @@ -13849,7 +13791,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13934,7 +13875,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -14015,7 +13955,6 @@ '00:00:00:00:00:00:aid:293334836', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'switchbot', @@ -14207,7 +14146,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -14288,7 +14226,6 @@ '00:00:00:00:00:00:aid:293334836', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'switchbot', @@ -14480,7 +14417,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -14561,7 +14497,6 @@ '00:00:00:00:00:00:aid:3982136094', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'FirstAlert', @@ -14761,7 +14696,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Garzola Marco', @@ -14974,7 +14908,6 @@ '00:00:00:00:00:00:aid:6623462395276914', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15120,7 +15053,6 @@ '00:00:00:00:00:00:aid:6623462395276939', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15266,7 +15198,6 @@ '00:00:00:00:00:00:aid:6623462403113447', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15412,7 +15343,6 @@ '00:00:00:00:00:00:aid:6623462403233419', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15558,7 +15488,6 @@ '00:00:00:00:00:00:aid:6623462412411853', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15714,7 +15643,6 @@ '00:00:00:00:00:00:aid:6623462412413293', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15870,7 +15798,6 @@ '00:00:00:00:00:00:aid:6623462389072572', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16203,7 +16130,6 @@ '00:00:00:00:00:00:aid:6623462378982941', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16336,7 +16262,6 @@ '00:00:00:00:00:00:aid:6623462378983942', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16469,7 +16394,6 @@ '00:00:00:00:00:00:aid:6623462379122122', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16602,7 +16526,6 @@ '00:00:00:00:00:00:aid:6623462379123707', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16735,7 +16658,6 @@ '00:00:00:00:00:00:aid:6623462383114163', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16868,7 +16790,6 @@ '00:00:00:00:00:00:aid:6623462383114193', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -17001,7 +16922,6 @@ '00:00:00:00:00:00:aid:6623462385996792', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -17134,7 +17054,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips Lighting', @@ -17219,7 +17138,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Koogeek', @@ -17371,7 +17289,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Koogeek', @@ -17549,7 +17466,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Koogeek', @@ -17768,7 +17684,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lennox', @@ -18067,7 +17982,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'LG Electronics', @@ -18258,7 +18172,6 @@ '00:00:00:00:00:00:aid:21474836482', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lutron Electronics Co., Inc', @@ -18390,7 +18303,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lutron Electronics Co., Inc', @@ -18475,7 +18387,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Meross', @@ -18770,7 +18681,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Meross', @@ -18907,7 +18817,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Empowered Homes Inc.', @@ -19256,7 +19165,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Nanoleaf', @@ -19540,7 +19448,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -19850,7 +19757,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -20021,7 +19927,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -20346,7 +20251,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Green Electronics LLC', @@ -20791,7 +20695,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -20964,7 +20867,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21045,7 +20947,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21222,7 +21123,6 @@ '00:00:00:00:00:00:aid:4', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21395,7 +21295,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21568,7 +21467,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21741,7 +21639,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21822,7 +21719,6 @@ '00:00:00:00:00:00:aid:5', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21999,7 +21895,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Schlage ', @@ -22127,7 +22022,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Hunter Fan', @@ -22316,7 +22210,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -22446,7 +22339,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Moen Incorporated', @@ -22860,7 +22752,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -23089,7 +22980,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VELUX', @@ -23170,7 +23060,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VELUX', @@ -23395,7 +23284,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VELUX', @@ -23525,7 +23413,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -23655,7 +23542,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -23784,7 +23670,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VOCOlinc', @@ -24104,7 +23989,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VOCOlinc', diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 166fd1a9e65..86c428b4413 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -330,6 +330,7 @@ async def test_snapshots( device_dict.pop("_cache", None) # This can be removed when suggested_area is removed from DeviceEntry device_dict.pop("_suggested_area") + device_dict.pop("is_new") devices.append({"device": device_dict, "entities": entities}) diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index 3b6264367e2..967672580ec 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -70,7 +70,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index b75b89269f1..972b7fc5728 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -79,7 +79,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -173,7 +172,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', diff --git a/tests/components/homewizard/snapshots/test_select.ambr b/tests/components/homewizard/snapshots/test_select.ambr index dd331c3f49b..0797256120c 100644 --- a/tests/components/homewizard/snapshots/test_select.ambr +++ b/tests/components/homewizard/snapshots/test_select.ambr @@ -80,7 +80,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index f870170bae9..1bde08b3201 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -21,7 +21,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -108,7 +107,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -200,7 +198,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -292,7 +289,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -384,7 +380,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -476,7 +471,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -568,7 +562,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -660,7 +653,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -745,7 +737,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -837,7 +828,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -925,7 +915,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1009,7 +998,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1101,7 +1089,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1193,7 +1180,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1285,7 +1271,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1377,7 +1362,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1469,7 +1453,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1561,7 +1544,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1650,7 +1632,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1742,7 +1723,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1834,7 +1814,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1918,7 +1897,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2006,7 +1984,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2098,7 +2075,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2190,7 +2166,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2282,7 +2257,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2374,7 +2348,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2466,7 +2439,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2558,7 +2530,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2650,7 +2621,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2742,7 +2712,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2834,7 +2803,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2926,7 +2894,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3018,7 +2985,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3110,7 +3076,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3199,7 +3164,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3288,7 +3252,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3377,7 +3340,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3469,7 +3431,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3561,7 +3522,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3653,7 +3613,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3745,7 +3704,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3837,7 +3795,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3929,7 +3886,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4021,7 +3977,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4113,7 +4068,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4205,7 +4159,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4297,7 +4250,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4381,7 +4333,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4469,7 +4420,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4558,7 +4508,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4650,7 +4599,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4742,7 +4690,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4834,7 +4781,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4918,7 +4864,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5010,7 +4955,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5102,7 +5046,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5194,7 +5137,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5286,7 +5228,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5378,7 +5319,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5470,7 +5410,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5562,7 +5501,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5654,7 +5592,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5746,7 +5683,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5838,7 +5774,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5930,7 +5865,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6014,7 +5948,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6103,7 +6036,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6195,7 +6127,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6279,7 +6210,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6371,7 +6301,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6463,7 +6392,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6555,7 +6483,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6639,7 +6566,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6723,7 +6649,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6821,7 +6746,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6913,7 +6837,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7005,7 +6928,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7097,7 +7019,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7189,7 +7110,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7273,7 +7193,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7357,7 +7276,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7441,7 +7359,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7525,7 +7442,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7609,7 +7525,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7693,7 +7608,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7781,7 +7695,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7865,7 +7778,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7949,7 +7861,6 @@ 'gas_meter_G001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8037,7 +7948,6 @@ 'heat_meter_H001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8125,7 +8035,6 @@ 'inlet_heat_meter_IH001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8209,7 +8118,6 @@ 'warm_water_meter_WW001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8297,7 +8205,6 @@ 'water_meter_W001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8389,7 +8296,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8478,7 +8384,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8570,7 +8475,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8662,7 +8566,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8754,7 +8657,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8838,7 +8740,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8930,7 +8831,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9022,7 +8922,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9114,7 +9013,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9206,7 +9104,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9298,7 +9195,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9390,7 +9286,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9482,7 +9377,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9574,7 +9468,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9666,7 +9559,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9758,7 +9650,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9850,7 +9741,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9934,7 +9824,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10023,7 +9912,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10115,7 +10003,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10199,7 +10086,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10291,7 +10177,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10383,7 +10268,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10475,7 +10359,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10559,7 +10442,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10643,7 +10525,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10741,7 +10622,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10833,7 +10713,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10925,7 +10804,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11017,7 +10895,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11109,7 +10986,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11193,7 +11069,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11277,7 +11152,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11361,7 +11235,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11445,7 +11318,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11529,7 +11401,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11613,7 +11484,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11701,7 +11571,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11785,7 +11654,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11869,7 +11737,6 @@ 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11957,7 +11824,6 @@ 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12045,7 +11911,6 @@ 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12129,7 +11994,6 @@ 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12217,7 +12081,6 @@ 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12309,7 +12172,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12398,7 +12260,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12490,7 +12351,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12582,7 +12442,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12674,7 +12533,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12766,7 +12624,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12858,7 +12715,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12950,7 +12806,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13042,7 +12897,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13134,7 +12988,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13226,7 +13079,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13318,7 +13170,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13410,7 +13261,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13502,7 +13352,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13594,7 +13443,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13686,7 +13534,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13770,7 +13617,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13862,7 +13708,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13946,7 +13791,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14038,7 +13882,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14130,7 +13973,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14222,7 +14064,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14314,7 +14155,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14406,7 +14246,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14498,7 +14337,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14590,7 +14428,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14674,7 +14511,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14758,7 +14594,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14842,7 +14677,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14926,7 +14760,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15010,7 +14843,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15094,7 +14926,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15182,7 +15013,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15266,7 +15096,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15354,7 +15183,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15446,7 +15274,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15538,7 +15365,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15630,7 +15456,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15722,7 +15547,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15806,7 +15630,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15894,7 +15717,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15986,7 +15808,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16078,7 +15899,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16170,7 +15990,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16262,7 +16081,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16354,7 +16172,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16446,7 +16263,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16535,7 +16351,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16627,7 +16442,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16719,7 +16533,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16811,7 +16624,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16895,7 +16707,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16983,7 +16794,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17075,7 +16885,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17163,7 +16972,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17247,7 +17055,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17335,7 +17142,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17427,7 +17233,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17519,7 +17324,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17611,7 +17415,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17703,7 +17506,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17795,7 +17597,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17887,7 +17688,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17976,7 +17776,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18068,7 +17867,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18160,7 +17958,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18244,7 +18041,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18332,7 +18128,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18424,7 +18219,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18516,7 +18310,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18608,7 +18401,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18700,7 +18492,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18792,7 +18583,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18884,7 +18674,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18976,7 +18765,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19068,7 +18856,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19160,7 +18947,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19252,7 +19038,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19344,7 +19129,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19436,7 +19220,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19525,7 +19308,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19614,7 +19396,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19703,7 +19484,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19795,7 +19575,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19887,7 +19666,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19979,7 +19757,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20071,7 +19848,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20163,7 +19939,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20255,7 +20030,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20347,7 +20121,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20439,7 +20212,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20531,7 +20303,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20623,7 +20394,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20707,7 +20477,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index 49916a59d9e..d61979c84b5 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -69,7 +69,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -153,7 +152,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -238,7 +236,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -322,7 +319,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -406,7 +402,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -491,7 +486,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -575,7 +569,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -659,7 +652,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -743,7 +735,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -827,7 +818,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -911,7 +901,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index e0627ad9da8..82116391f4f 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'c7233734-b219-4287-a173-08e3643f89f0', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Husqvarna', diff --git a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr index e2b8eeba811..6b4ab8236f9 100644 --- a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '00000000-0000-0000-0000-000000000003_1197489078', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Husqvarna', diff --git a/tests/components/iotty/snapshots/test_switch.ambr b/tests/components/iotty/snapshots/test_switch.ambr index 04712dbf022..41e79911154 100644 --- a/tests/components/iotty/snapshots/test_switch.ambr +++ b/tests/components/iotty/snapshots/test_switch.ambr @@ -29,7 +29,6 @@ 'TestLS', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'iotty', diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index 6a5f5371a9d..02076bf5597 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '26e93f1a-c828-11ea-87d0-0242ac130003', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ista SE', @@ -49,7 +48,6 @@ 'eaf5c5c8-889f-4a3c-b68c-e9a676505762', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ista SE', diff --git a/tests/components/ituran/snapshots/test_init.ambr b/tests/components/ituran/snapshots/test_init.ambr index 456687407e2..5fb786029b4 100644 --- a/tests/components/ituran/snapshots/test_init.ambr +++ b/tests/components/ituran/snapshots/test_init.ambr @@ -18,7 +18,6 @@ '12345678', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'mock make', diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 350ac169938..2bee2f1f61c 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -65,7 +65,6 @@ 'outlet_1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -97,7 +96,6 @@ '2_ch_power_strip', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -177,7 +175,6 @@ 'outlet_2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -209,7 +206,6 @@ '2_ch_power_strip', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/lamarzocco/snapshots/test_init.ambr b/tests/components/lamarzocco/snapshots/test_init.ambr index f11057f8620..bdebd35d6dd 100644 --- a/tests/components/lamarzocco/snapshots/test_init.ambr +++ b/tests/components/lamarzocco/snapshots/test_init.ambr @@ -25,7 +25,6 @@ 'GS012345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'La Marzocco', diff --git a/tests/components/lektrico/snapshots/test_init.ambr b/tests/components/lektrico/snapshots/test_init.ambr index a935f5cfa14..e1b5a48fe27 100644 --- a/tests/components/lektrico/snapshots/test_init.ambr +++ b/tests/components/lektrico/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '500006', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Lektrico', diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr index 4d3e9d7aeab..662ffd51cb4 100644 --- a/tests/components/mastodon/snapshots/test_init.ambr +++ b/tests/components/mastodon/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'trwnh_mastodon_social', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Mastodon gGmbH', diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index e3a9e608911..50da06ca005 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'bf1c62fe-4941-4332-9886-e54e88dbdba0', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/meater/snapshots/test_init.ambr b/tests/components/meater/snapshots/test_init.ambr index 95335942de6..654e631cdda 100644 --- a/tests/components/meater/snapshots/test_init.ambr +++ b/tests/components/meater/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Apption Labs', diff --git a/tests/components/miele/snapshots/test_init.ambr b/tests/components/miele/snapshots/test_init.ambr index 9feeeb6523b..81f6c0c3a35 100644 --- a/tests/components/miele/snapshots/test_init.ambr +++ b/tests/components/miele/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'Dummy_Appliance_1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Miele', diff --git a/tests/components/myuplink/snapshots/test_init.ambr b/tests/components/myuplink/snapshots/test_init.ambr index 56fb26b4084..66b4c9efe35 100644 --- a/tests/components/myuplink/snapshots/test_init.ambr +++ b/tests/components/myuplink/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'alfred-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Jäspi', @@ -49,7 +48,6 @@ 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Nibe', @@ -81,7 +79,6 @@ 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Nibe', diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index 95fb1f9ed45..3f8d924bdbf 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '0009999992', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Bubbendorf', @@ -49,7 +48,6 @@ '0009999993', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Bubbendorf', @@ -81,7 +79,6 @@ '00:11:22:33:00:11:45:fe', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -113,7 +110,6 @@ '1002003001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Smarther', @@ -145,7 +141,6 @@ '12:34:56:00:00:a1:4c:da', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -177,7 +172,6 @@ '12:34:56:00:01:01:01:a1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -209,7 +203,6 @@ '12:34:56:00:16:0e#0', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -241,7 +234,6 @@ '12:34:56:00:16:0e#1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -273,7 +265,6 @@ '12:34:56:00:16:0e#2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -305,7 +296,6 @@ '12:34:56:00:16:0e#3', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -337,7 +327,6 @@ '12:34:56:00:16:0e#4', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -369,7 +358,6 @@ '12:34:56:00:16:0e#5', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -401,7 +389,6 @@ '12:34:56:00:16:0e#6', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -433,7 +420,6 @@ '12:34:56:00:16:0e#7', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -465,7 +451,6 @@ '12:34:56:00:16:0e#8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -497,7 +482,6 @@ '12:34:56:00:16:0e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -529,7 +513,6 @@ '12:34:56:00:f1:62', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -561,7 +544,6 @@ '12:34:56:03:1b:e4', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -593,7 +575,6 @@ '12:34:56:10:b9:0e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -625,7 +606,6 @@ '12:34:56:10:f1:66', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -657,7 +637,6 @@ '12:34:56:25:cf:a8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -689,7 +668,6 @@ '12:34:56:26:65:14', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -721,7 +699,6 @@ '12:34:56:26:68:92', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -753,7 +730,6 @@ '12:34:56:26:69:0c', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -785,7 +761,6 @@ '12:34:56:3e:c5:46', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -817,7 +792,6 @@ '12:34:56:80:00:12:ac:f2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -849,7 +823,6 @@ '12:34:56:80:1c:42', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -881,7 +854,6 @@ '12:34:56:80:44:92', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -913,7 +885,6 @@ '12:34:56:80:7e:18', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -945,7 +916,6 @@ '12:34:56:80:bb:26', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -977,7 +947,6 @@ '12:34:56:80:c1:ea', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1009,7 +978,6 @@ '222452125', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1041,7 +1009,6 @@ '2746182631', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1073,7 +1040,6 @@ '2833524037', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1105,7 +1071,6 @@ '2940411577', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1137,7 +1102,6 @@ '91763b24c43d3e344f424e8b', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1169,7 +1133,6 @@ 'Home avg', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1201,7 +1164,6 @@ 'Home max', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1233,7 +1195,6 @@ 'Home min', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index 2980e3f35f0..fd58e6e0002 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'FFFFFFFFFFFFF', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netgear', diff --git a/tests/components/nyt_games/snapshots/test_init.ambr b/tests/components/nyt_games/snapshots/test_init.ambr index 5ca9a2d8df2..f920b064f0b 100644 --- a/tests/components/nyt_games/snapshots/test_init.ambr +++ b/tests/components/nyt_games/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '218886794_connections', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'New York Times', @@ -49,7 +48,6 @@ '218886794_spelling_bee', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'New York Times', @@ -81,7 +79,6 @@ '218886794_wordle', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'New York Times', diff --git a/tests/components/ohme/snapshots/test_init.ambr b/tests/components/ohme/snapshots/test_init.ambr index 2e8304489d9..dc49f5f4042 100644 --- a/tests/components/ohme/snapshots/test_init.ambr +++ b/tests/components/ohme/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'chargerid', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ohme', diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index 787551ad90e..6ea2ad11103 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'W1122333044455', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ondilo', @@ -49,7 +48,6 @@ 'W2233304445566', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ondilo', diff --git a/tests/components/onedrive/snapshots/test_init.ambr b/tests/components/onedrive/snapshots/test_init.ambr index 2f9cfc1a038..2573c34e1fa 100644 --- a/tests/components/onedrive/snapshots/test_init.ambr +++ b/tests/components/onedrive/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'mock_drive_id', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Microsoft', diff --git a/tests/components/onewire/snapshots/test_init.ambr b/tests/components/onewire/snapshots/test_init.ambr index 26ed15fc897..b879541d4ca 100644 --- a/tests/components/onewire/snapshots/test_init.ambr +++ b/tests/components/onewire/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '05.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -49,7 +48,6 @@ '10.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -81,7 +79,6 @@ '12.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -113,7 +110,6 @@ '1D.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -145,7 +141,6 @@ '1F.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -177,7 +172,6 @@ '20.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -209,7 +203,6 @@ '22.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -241,7 +234,6 @@ '26.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -273,7 +265,6 @@ '28.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -305,7 +296,6 @@ '28.222222222222', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -337,7 +327,6 @@ '28.222222222223', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -369,7 +358,6 @@ '29.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -401,7 +389,6 @@ '30.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -433,7 +420,6 @@ '3A.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -465,7 +451,6 @@ '3B.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -497,7 +482,6 @@ '42.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -529,7 +513,6 @@ '7E.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Embedded Data Systems', @@ -561,7 +544,6 @@ '7E.222222222222', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Embedded Data Systems', @@ -593,7 +575,6 @@ 'A6.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -625,7 +606,6 @@ 'EF.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Hobby Boards', @@ -657,7 +637,6 @@ 'EF.111111111112', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Hobby Boards', @@ -689,7 +668,6 @@ 'EF.111111111113', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Hobby Boards', diff --git a/tests/components/openai_conversation/snapshots/test_init.ambr b/tests/components/openai_conversation/snapshots/test_init.ambr index 0058416b254..f5006ac979f 100644 --- a/tests/components/openai_conversation/snapshots/test_init.ambr +++ b/tests/components/openai_conversation/snapshots/test_init.ambr @@ -11,7 +11,6 @@ 'entry_type': , 'hw_version': None, 'id': , - 'is_new': False, 'labels': set({ }), 'manufacturer': 'OpenAI', @@ -37,7 +36,6 @@ 'entry_type': , 'hw_version': None, 'id': , - 'is_new': False, 'labels': set({ }), 'manufacturer': 'OpenAI', diff --git a/tests/components/overseerr/snapshots/test_init.ambr b/tests/components/overseerr/snapshots/test_init.ambr index 71c1b9ffd3a..f861ccaa9ed 100644 --- a/tests/components/overseerr/snapshots/test_init.ambr +++ b/tests/components/overseerr/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '01JG00V55WEVTJ0CJHM0GAD7PC', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/palazzetti/snapshots/test_init.ambr b/tests/components/palazzetti/snapshots/test_init.ambr index b69982d9c08..3fca1d851ce 100644 --- a/tests/components/palazzetti/snapshots/test_init.ambr +++ b/tests/components/palazzetti/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'id': , 'identifiers': set({ }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Palazzetti', diff --git a/tests/components/peblar/snapshots/test_init.ambr b/tests/components/peblar/snapshots/test_init.ambr index 97c0737e402..21edc32c629 100644 --- a/tests/components/peblar/snapshots/test_init.ambr +++ b/tests/components/peblar/snapshots/test_init.ambr @@ -25,7 +25,6 @@ '23-45-A4O-MOF', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Peblar', diff --git a/tests/components/rainforest_raven/snapshots/test_init.ambr b/tests/components/rainforest_raven/snapshots/test_init.ambr index f34d33d6c24..9cc89cfcc9e 100644 --- a/tests/components/rainforest_raven/snapshots/test_init.ambr +++ b/tests/components/rainforest_raven/snapshots/test_init.ambr @@ -22,7 +22,6 @@ 'abcdef0123456789', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Rainforest Automation, Inc.', diff --git a/tests/components/renault/snapshots/test_init.ambr b/tests/components/renault/snapshots/test_init.ambr index defb0f249ff..15b3c599711 100644 --- a/tests/components/renault/snapshots/test_init.ambr +++ b/tests/components/renault/snapshots/test_init.ambr @@ -18,7 +18,6 @@ 'VF1CAPTURFUELVIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -52,7 +51,6 @@ 'VF1CAPTURPHEVVIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -86,7 +84,6 @@ 'VF1TWINGOIIIVIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -120,7 +117,6 @@ 'VF1ZOE40VIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -154,7 +150,6 @@ 'VF1ZOE50VIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 3715f994fb0..25925ac3865 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '8381BE13', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/russound_rio/snapshots/test_init.ambr b/tests/components/russound_rio/snapshots/test_init.ambr index 0fcebb8a6e5..b02f80f1dfd 100644 --- a/tests/components/russound_rio/snapshots/test_init.ambr +++ b/tests/components/russound_rio/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '00:11:22:33:44:55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Russound', diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index f9006c7fd52..4be166ecf25 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -22,7 +22,6 @@ 'be9554b9-c9fb-41f4-8920-22da015376a4', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -56,7 +55,6 @@ '123456', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -94,7 +92,6 @@ 'be9554b9-c9fb-41f4-8920-22da015376a4', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/schlage/snapshots/test_init.ambr b/tests/components/schlage/snapshots/test_init.ambr index 4e57ad5d5c6..1b6cc3f1cdb 100644 --- a/tests/components/schlage/snapshots/test_init.ambr +++ b/tests/components/schlage/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'test', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Schlage', diff --git a/tests/components/sensibo/snapshots/test_entity.ambr b/tests/components/sensibo/snapshots/test_entity.ambr index ee0b3835da4..ba075d764f5 100644 --- a/tests/components/sensibo/snapshots/test_entity.ambr +++ b/tests/components/sensibo/snapshots/test_entity.ambr @@ -22,7 +22,6 @@ 'ABC999111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -56,7 +55,6 @@ 'AAZZAAZZ', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -90,7 +88,6 @@ 'BBZZBBZZ', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -120,7 +117,6 @@ 'AABBCC', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index f0193b6ce1c..f046f95ed42 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -18,7 +18,6 @@ 'e4:5d:51:00:11:22', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -150,7 +149,6 @@ 'e4:5d:51:00:11:22', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index e3e5475ca34..5d5c6d0edba 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -18,7 +18,6 @@ 'e4:5d:51:00:11:22', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 681c3a84191..0440505859a 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -18,7 +18,6 @@ 'e4:5d:51:00:11:22', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/slide_local/snapshots/test_init.ambr b/tests/components/slide_local/snapshots/test_init.ambr index e2dec748e2a..cc93a49b98a 100644 --- a/tests/components/slide_local/snapshots/test_init.ambr +++ b/tests/components/slide_local/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'id': , 'identifiers': set({ }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Innovation in Motion', diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index d63ac4e9ab4..17a9d6691cc 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -20,7 +20,6 @@ '7c16163e-c94e-482f-95f6-139ae0cd9d5e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -52,7 +51,6 @@ 'f0af21a2-d5a1-437c-b10a-b34a87394b71', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -84,7 +82,6 @@ 'bf53a150-f8a4-45d1-aac4-86252475d551', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -116,7 +113,6 @@ '68e786a6-7f61-4c3a-9e13-70b803cf782b', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -148,7 +144,6 @@ '286ba274-4093-4bcb-849c-a1a3efe7b1e5', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -180,7 +175,6 @@ '10e06a70-ee7d-4832-85e9-a0a06a7a05bd', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Arlo', @@ -212,7 +206,6 @@ '571af102-15db-4030-b76b-245a691f74a5', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WonderLabs Company', @@ -244,7 +237,6 @@ 'd0268a69-abfb-4c92-a646-61cec2e510ad', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -276,7 +268,6 @@ '2d9a892b-1c93-45a5-84cb-0e81889498c6', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -308,7 +299,6 @@ 'a3a970ea-e09c-9c04-161b-94c934e21666', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -340,7 +330,6 @@ '4165c51e-bf6b-c5b6-fd53-127d6248754b', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -372,7 +361,6 @@ '96a5ef74-5832-a84b-f1f7-ca799957065d', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -404,7 +392,6 @@ 'c76d6f38-1b7f-13dd-37b5-db18d5272783', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -436,7 +423,6 @@ '4ece486b-89db-f06a-d54d-748b676b4d8e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -468,7 +454,6 @@ 'F8042E25-0E53-0000-0000-000000000000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -500,7 +485,6 @@ '808dbd84-f357-47e2-a0cd-3b66fa22d584', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -532,7 +516,6 @@ '2bad3237-4886-e699-1b90-4a51a3d55c8a', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -564,7 +547,6 @@ '9447959a-0dfa-6b27-d40d-650da525c53f', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -596,7 +578,6 @@ '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -628,7 +609,6 @@ '7db87911-7dce-1cf2-7119-b953432a2f09', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -660,7 +640,6 @@ '7d3feb98-8a36-4351-c362-5e21ad3a78dd', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -692,7 +671,6 @@ '5758b2ec-563e-f39b-ec39-208e54aabf60', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -724,7 +702,6 @@ '05accb39-2017-c98b-a5ab-04a81f4d3d9a', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -756,7 +733,6 @@ '3442dfc6-17c0-a65f-dae0-4c6e01786f44', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -788,7 +764,6 @@ '1f98ebd0-ac48-d802-7f62-000001200100', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -820,7 +795,6 @@ '6a7d5349-0a66-0277-058d-000001200101', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -852,7 +826,6 @@ '3810e5ad-5351-d9f9-12ff-000001200000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -884,7 +857,6 @@ 'f36dc7ce-cac0-0667-dc14-a3704eb5e676', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -916,7 +888,6 @@ 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -948,7 +919,6 @@ '02f7256e-8353-5bdd-547f-bd5b1647e01b', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -980,7 +950,6 @@ '3a6c4e05-811d-5041-e956-3d04c424cbcd', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1012,7 +981,6 @@ 'f984b91d-f250-9d42-3436-33f09a422a47', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1044,7 +1012,6 @@ '63803fae-cbed-f356-a063-2cf148ae3ca7', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1076,7 +1043,6 @@ 'b854ca5f-dc54-140d-6349-758b4d973c41', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1108,7 +1074,6 @@ 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1140,7 +1105,6 @@ 'd5dc3299-c266-41c7-bd08-f540aea54b89', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ecobee', @@ -1172,7 +1136,6 @@ '028469cb-6e89-4f14-8d9a-bfbca5e0fbfc', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ecobee', @@ -1204,7 +1167,6 @@ '1888b38f-6246-4f1e-911b-bfcfb66999db', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ecobee', @@ -1236,7 +1198,6 @@ 'f1af21a2-d5a1-437c-b10a-b34a87394b71', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1268,7 +1229,6 @@ '3b57dca3-9a90-4f27-ba80-f947b1e60d58', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'CopperLabs', @@ -1300,7 +1260,6 @@ 'aaedaf28-2ae0-4c1d-b57e-87f6a420c298', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1332,7 +1291,6 @@ '656569c2-7976-4232-a789-34b4d1176c3a', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1364,7 +1322,6 @@ '6d95a8b7-4ee3-429a-a13a-00ec9354170c', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1396,7 +1353,6 @@ '5e5b97f3-3094-44e6-abc0-f61283412d6a', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1428,7 +1384,6 @@ '69a271f6-6537-4982-8cd9-979866872692', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1460,7 +1415,6 @@ '440063de-a200-40b5-8a6b-f3399eaa0370', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Signify Netherlands B.V.', @@ -1492,7 +1446,6 @@ 'cb958955-b015-498c-9e62-fc0c51abd054', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Signify Netherlands B.V.', @@ -1524,7 +1477,6 @@ 'afcf3b91-0000-1111-2222-ddff2a0a6577', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1556,7 +1508,6 @@ '71afed1c-006d-4e48-b16e-e7f88f9fd638', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1588,7 +1539,6 @@ '83d660e4-b0c8-4881-a674-d9f1730366c1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1620,7 +1570,6 @@ 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1652,7 +1601,6 @@ '184c67cc-69e2-44b6-8f73-55c963068ad9', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1684,7 +1632,6 @@ '692ea4e9-2022-4ed8-8a57-1b884a59cc38', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1716,7 +1663,6 @@ '7d246592-93db-4d72-a10d-5a51793ece8c', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1748,7 +1694,6 @@ '2409a73c-918a-4d1f-b4f5-c27468c71d70', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Emerson', @@ -1780,7 +1725,6 @@ 'bf4b1167-48a3-4af7-9186-0900a678ffa5', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -1812,7 +1756,6 @@ '550a1c72-65a0-4d55-b97b-75168e055398', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1844,7 +1787,6 @@ 'c85fced9-c474-4a47-93c2-037cc7829536', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1876,7 +1818,6 @@ '6602696a-1e48-49e4-919f-69406f5b5da1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -1908,7 +1849,6 @@ '0d94e5db-8501-2355-eb4f-214163702cac', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1940,7 +1880,6 @@ 'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1972,7 +1911,6 @@ '5cc1c096-98b9-460c-8f1c-1045509ec605', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -2004,7 +1942,6 @@ '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -2036,7 +1973,6 @@ '2894dc93-0f11-49cc-8a81-3a684cebebf6', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2068,7 +2004,6 @@ '612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2100,7 +2035,6 @@ 'a2a6018b-2663-4727-9d1d-8f56953b5116', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2132,7 +2066,6 @@ 'a9f587c5-5d8b-4273-8907-e7f609af5158', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2168,7 +2101,6 @@ '074fa784-8be8-4c70-8e22-6f5ed6f81b7e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/smarty/snapshots/test_init.ambr b/tests/components/smarty/snapshots/test_init.ambr index ffa30051726..989e95bde7e 100644 --- a/tests/components/smarty/snapshots/test_init.ambr +++ b/tests/components/smarty/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '01JAZ5DPW8C62D620DGYNG2R8H', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Salda', diff --git a/tests/components/smlight/snapshots/test_init.ambr b/tests/components/smlight/snapshots/test_init.ambr index 8f533a42e36..7f46daef13c 100644 --- a/tests/components/smlight/snapshots/test_init.ambr +++ b/tests/components/smlight/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'id': , 'identifiers': set({ }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'SMLIGHT', diff --git a/tests/components/squeezebox/snapshots/test_init.ambr b/tests/components/squeezebox/snapshots/test_init.ambr index 39664f9ecf2..afd90d026de 100644 --- a/tests/components/squeezebox/snapshots/test_init.ambr +++ b/tests/components/squeezebox/snapshots/test_init.ambr @@ -21,7 +21,6 @@ 'aa:bb:cc:dd:ee:ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ralph Irving & Adrian Smith', @@ -61,7 +60,6 @@ 'ff:ee:dd:cc:bb:aa', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'https://lyrion.org/ / Ralph Irving & Adrian Smith', diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index a5a591af94c..2cf93435bbf 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -66,7 +66,6 @@ '_3c_e9_e_6d_21_84_-door1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', @@ -147,7 +146,6 @@ '_3c_e9_e_6d_21_84_-door2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 627f05432d2..12b99997db0 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -70,7 +70,6 @@ '_3c_e9_e_6d_21_84_', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index 54c648ba21b..a14dcfc44f1 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -67,7 +67,6 @@ '_3c_e9_e_6d_21_84_-door1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', @@ -149,7 +148,6 @@ '_3c_e9_e_6d_21_84_-door2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index acabe061420..f9132530cee 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -79,7 +79,6 @@ '_3c_e9_e_6d_21_84_', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index cfaca7b81f3..38874d08f3a 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '0000-0000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tedee', @@ -49,7 +48,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tedee', diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index b7bf9e6bfa5..a73e5c746aa 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -66,7 +66,6 @@ '98765', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tedee', diff --git a/tests/components/tesla_fleet/snapshots/test_init.ambr b/tests/components/tesla_fleet/snapshots/test_init.ambr index a669813a3a5..7ce99965900 100644 --- a/tests/components/tesla_fleet/snapshots/test_init.ambr +++ b/tests/components/tesla_fleet/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '123456', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -49,7 +48,6 @@ 'LRWXF7EK4KC700000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -81,7 +79,6 @@ 'abd-123', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -113,7 +110,6 @@ 'bcd-234', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index 39fc8d04984..ee27bc9f0af 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '123456', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -49,7 +48,6 @@ 'LRW3F7EK4NC700000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -81,7 +79,6 @@ 'abd-123', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -113,7 +110,6 @@ 'bcd-234', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', diff --git a/tests/components/tile/snapshots/test_init.ambr b/tests/components/tile/snapshots/test_init.ambr index 0c3e1faf090..9e2620313a0 100644 --- a/tests/components/tile/snapshots/test_init.ambr +++ b/tests/components/tile/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '19264d2dffdbca32', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tile Inc.', diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index c12f73bd737..ed5f935f286 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -420,7 +420,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr index 7d49d2aedbc..37cfb4a36c0 100644 --- a/tests/components/tplink/snapshots/test_button.ambr +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -602,7 +602,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_camera.ambr b/tests/components/tplink/snapshots/test_camera.ambr index a0282401e58..b17b30bbbb4 100644 --- a/tests/components/tplink/snapshots/test_camera.ambr +++ b/tests/components/tplink/snapshots/test_camera.ambr @@ -72,7 +72,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index 4a38bdbbe59..01738bff943 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -82,7 +82,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index eb42e2a7298..b48ef8d336b 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -186,7 +186,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index bc71313bf96..da4dc26317b 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr index 6bcd24521e4..1db6e3cf57d 100644 --- a/tests/components/tplink/snapshots/test_select.ambr +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index f95390a8a57..05d645552bb 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_siren.ambr b/tests/components/tplink/snapshots/test_siren.ambr index 7f90915f624..45bad203bc9 100644 --- a/tests/components/tplink/snapshots/test_siren.ambr +++ b/tests/components/tplink/snapshots/test_siren.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index 98584c79759..6d1d2fa3bad 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tplink/snapshots/test_vacuum.ambr b/tests/components/tplink/snapshots/test_vacuum.ambr index e5b28f5ac7a..e3a7f1d95fe 100644 --- a/tests/components/tplink/snapshots/test_vacuum.ambr +++ b/tests/components/tplink/snapshots/test_vacuum.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index fc30460bcc0..127c764ea0d 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -18,7 +18,6 @@ 'mock_device_id', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tuya', diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 68f5a7b6adf..15fcd7cee09 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -97,7 +97,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index ad435a833ee..3b4e21be1e1 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -66,7 +66,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -147,7 +146,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -228,7 +226,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -309,7 +306,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -390,7 +386,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index cb4563e0fb5..c57f2987c5b 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -59,7 +59,6 @@ 'entry_type': , 'hw_version': None, 'id': , - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/velbus/snapshots/test_init.ambr b/tests/components/velbus/snapshots/test_init.ambr index 29f92126f95..0383abc0313 100644 --- a/tests/components/velbus/snapshots/test_init.ambr +++ b/tests/components/velbus/snapshots/test_init.ambr @@ -18,7 +18,6 @@ '1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -48,7 +47,6 @@ '2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -78,7 +76,6 @@ '88', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -108,7 +105,6 @@ '88-10', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -138,7 +134,6 @@ '88-11', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -168,7 +163,6 @@ '88-2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -198,7 +192,6 @@ '88-3', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -228,7 +221,6 @@ '88-33', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -258,7 +250,6 @@ '88-55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -288,7 +279,6 @@ '88-9', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 212535862f5..86cfa8198ba 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -18,7 +18,6 @@ 'air-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -118,7 +117,6 @@ 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -217,7 +215,6 @@ '400s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -318,7 +315,6 @@ '600s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -419,7 +415,6 @@ 'dimmable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -457,7 +452,6 @@ 'dimmable-switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -495,7 +489,6 @@ '200s-humidifier4321', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -533,7 +526,6 @@ '600s-humidifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -571,7 +563,6 @@ 'outlet', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -609,7 +600,6 @@ 'smarttowerfan', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -714,7 +704,6 @@ 'tunable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -752,7 +741,6 @@ 'switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index eac595cc0e9..df2dad8825d 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -18,7 +18,6 @@ 'air-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -56,7 +55,6 @@ 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -94,7 +92,6 @@ '400s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -132,7 +129,6 @@ '600s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -170,7 +166,6 @@ 'dimmable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -262,7 +257,6 @@ 'dimmable-switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -356,7 +350,6 @@ '200s-humidifier4321', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -394,7 +387,6 @@ '600s-humidifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -432,7 +424,6 @@ 'outlet', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -470,7 +461,6 @@ 'smarttowerfan', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -508,7 +498,6 @@ 'tunable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -615,7 +604,6 @@ 'switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 6aa25e0763a..143520b68c2 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -18,7 +18,6 @@ 'air-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -152,7 +151,6 @@ 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -240,7 +238,6 @@ '400s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -425,7 +422,6 @@ '600s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -610,7 +606,6 @@ 'dimmable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -648,7 +643,6 @@ 'dimmable-switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -686,7 +680,6 @@ '200s-humidifier4321', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -775,7 +768,6 @@ '600s-humidifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -864,7 +856,6 @@ 'outlet', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -1226,7 +1217,6 @@ 'smarttowerfan', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -1264,7 +1254,6 @@ 'tunable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -1302,7 +1291,6 @@ 'switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 8947ac40424..e7917397063 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -18,7 +18,6 @@ 'air-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -102,7 +101,6 @@ 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -186,7 +184,6 @@ '400s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -270,7 +267,6 @@ '600s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -354,7 +350,6 @@ 'dimmable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -392,7 +387,6 @@ 'dimmable-switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -430,7 +424,6 @@ '200s-humidifier4321', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -514,7 +507,6 @@ '600s-humidifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -598,7 +590,6 @@ 'outlet', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -683,7 +674,6 @@ 'smarttowerfan', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -767,7 +757,6 @@ 'tunable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -805,7 +794,6 @@ 'switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', diff --git a/tests/components/webostv/snapshots/test_media_player.ambr b/tests/components/webostv/snapshots/test_media_player.ambr index d0a1142618a..7c0bdfb0d13 100644 --- a/tests/components/webostv/snapshots/test_media_player.ambr +++ b/tests/components/webostv/snapshots/test_media_player.ambr @@ -53,7 +53,6 @@ 'some-fake-uuid', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'LG', diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 38f125ad712..30cdb9080f8 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -65,7 +65,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -146,7 +145,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -231,7 +229,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -312,7 +309,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -393,7 +389,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -473,7 +468,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -553,7 +547,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -633,7 +626,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -713,7 +705,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -845,7 +836,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, diff --git a/tests/components/withings/snapshots/test_init.ambr b/tests/components/withings/snapshots/test_init.ambr index 88d9ff94b1d..31c23987680 100644 --- a/tests/components/withings/snapshots/test_init.ambr +++ b/tests/components/withings/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Withings', @@ -49,7 +48,6 @@ 'f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Withings', diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index 26f8817fa06..b7bb8a1eea1 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -70,7 +70,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index 5503b9a733d..8f94c270984 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -78,7 +78,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -171,7 +170,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index dc8a2f09445..a981b741852 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -80,7 +80,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -311,7 +310,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -404,7 +402,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -497,7 +494,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index 09c86d81d44..43e91f7b485 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -71,7 +71,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -155,7 +154,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -240,7 +238,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -325,7 +322,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', diff --git a/tests/components/wmspro/snapshots/test_cover.ambr b/tests/components/wmspro/snapshots/test_cover.ambr index 026785c9e1c..8590c4ba725 100644 --- a/tests/components/wmspro/snapshots/test_cover.ambr +++ b/tests/components/wmspro/snapshots/test_cover.ambr @@ -17,7 +17,6 @@ '58717', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', diff --git a/tests/components/wmspro/snapshots/test_init.ambr b/tests/components/wmspro/snapshots/test_init.ambr index 9d60cf8c907..ee485fe3980 100644 --- a/tests/components/wmspro/snapshots/test_init.ambr +++ b/tests/components/wmspro/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '19239', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -49,7 +48,6 @@ '58717', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -81,7 +79,6 @@ '97358', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -113,7 +110,6 @@ '19239', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -145,7 +141,6 @@ '58717', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -177,7 +172,6 @@ '97358', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -209,7 +203,6 @@ '116682', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -241,7 +234,6 @@ '172555', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -273,7 +265,6 @@ '18894', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -305,7 +296,6 @@ '230952', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -337,7 +327,6 @@ '284942', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -369,7 +358,6 @@ '328518', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', diff --git a/tests/components/wmspro/snapshots/test_light.ambr b/tests/components/wmspro/snapshots/test_light.ambr index 694fb2d51e4..9efbadff951 100644 --- a/tests/components/wmspro/snapshots/test_light.ambr +++ b/tests/components/wmspro/snapshots/test_light.ambr @@ -17,7 +17,6 @@ '97358', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', diff --git a/tests/components/wmspro/snapshots/test_scene.ambr b/tests/components/wmspro/snapshots/test_scene.ambr index 97f47dc7f15..b9053992ddc 100644 --- a/tests/components/wmspro/snapshots/test_scene.ambr +++ b/tests/components/wmspro/snapshots/test_scene.ambr @@ -31,7 +31,6 @@ '42581', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', diff --git a/tests/components/wolflink/snapshots/test_sensor.ambr b/tests/components/wolflink/snapshots/test_sensor.ambr index 3d6e3fea3b5..d66c1d2285b 100644 --- a/tests/components/wolflink/snapshots/test_sensor.ambr +++ b/tests/components/wolflink/snapshots/test_sensor.ambr @@ -17,7 +17,6 @@ '1234', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WOLF GmbH', diff --git a/tests/components/yale/snapshots/test_binary_sensor.ambr b/tests/components/yale/snapshots/test_binary_sensor.ambr index df0e604c550..226d0bdbba9 100644 --- a/tests/components/yale/snapshots/test_binary_sensor.ambr +++ b/tests/components/yale/snapshots/test_binary_sensor.ambr @@ -17,7 +17,6 @@ 'tmt100', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Yale Home Inc.', diff --git a/tests/components/yale/snapshots/test_lock.ambr b/tests/components/yale/snapshots/test_lock.ambr index dd2faa8b69e..3f89fe08525 100644 --- a/tests/components/yale/snapshots/test_lock.ambr +++ b/tests/components/yale/snapshots/test_lock.ambr @@ -21,7 +21,6 @@ 'online_with_doorsense', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Yale Home Inc.', diff --git a/tests/helpers/snapshots/test_entity_platform.ambr b/tests/helpers/snapshots/test_entity_platform.ambr index ce0abffd03c..2da81a95602 100644 --- a/tests/helpers/snapshots/test_entity_platform.ambr +++ b/tests/helpers/snapshots/test_entity_platform.ambr @@ -21,7 +21,6 @@ '1234', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'test-manuf', @@ -57,7 +56,6 @@ 'efgh', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'test-manuf', diff --git a/tests/syrupy.py b/tests/syrupy.py index 642e5a519b2..919ba1a6cea 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -175,6 +175,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): serialized.pop("_cache") # This can be removed when suggested_area is removed from DeviceEntry serialized.pop("_suggested_area") + serialized.pop("is_new") return cls._remove_created_and_modified_at(serialized) @classmethod From bc87140a6f85d6acb2d8afb5255b3dad0115ed30 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 1 Aug 2025 11:15:49 +0200 Subject: [PATCH 185/265] Update after Motion Blinds tilt change (#149779) --- .../components/motion_blinds/cover.py | 14 +++++++++++++ .../components/motion_blinds/entity.py | 21 +++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 9cff2956a5f..04adc9f2d60 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -289,17 +289,23 @@ class MotionTiltDevice(MotionPositionDevice): async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, 180) + await self.async_request_position_till_stop() + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, 0) + await self.async_request_position_till_stop() + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" angle = kwargs[ATTR_TILT_POSITION] * 180 / 100 async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, angle) + await self.async_request_position_till_stop() + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover.""" async with self._api_lock: @@ -360,11 +366,15 @@ class MotionTiltOnlyDevice(MotionTiltDevice): async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Open) + await self.async_request_position_till_stop() + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Close) + await self.async_request_position_till_stop() + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" angle = kwargs[ATTR_TILT_POSITION] @@ -376,6 +386,8 @@ class MotionTiltOnlyDevice(MotionTiltDevice): async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_position, angle) + await self.async_request_position_till_stop() + async def async_set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position (see TDBU).""" angle = kwargs.get(ATTR_TILT_POSITION) @@ -390,6 +402,8 @@ class MotionTiltOnlyDevice(MotionTiltDevice): async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_position, angle) + await self.async_request_position_till_stop() + class MotionTDBUDevice(MotionBaseDevice): """Representation of a Motion Top Down Bottom Up blind Device.""" diff --git a/homeassistant/components/motion_blinds/entity.py b/homeassistant/components/motion_blinds/entity.py index 483a638a0eb..9b52cbb01f5 100644 --- a/homeassistant/components/motion_blinds/entity.py +++ b/homeassistant/components/motion_blinds/entity.py @@ -42,6 +42,7 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind self._requesting_position: CALLBACK_TYPE | None = None self._previous_positions: list[int | dict | None] = [] + self._previous_angles: list[int | None] = [] if blind.device_type in DEVICE_TYPES_WIFI: self._update_interval_moving = UPDATE_INTERVAL_MOVING_WIFI @@ -112,17 +113,27 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind """Request a state update from the blind at a scheduled point in time.""" # add the last position to the list and keep the list at max 2 items self._previous_positions.append(self._blind.position) + self._previous_angles.append(self._blind.angle) if len(self._previous_positions) > 2: del self._previous_positions[: len(self._previous_positions) - 2] + if len(self._previous_angles) > 2: + del self._previous_angles[: len(self._previous_angles) - 2] async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Update_trigger) self.coordinator.async_update_listeners() - if len(self._previous_positions) < 2 or not all( - self._blind.position == prev_position - for prev_position in self._previous_positions + if ( + len(self._previous_positions) < 2 + or not all( + self._blind.position == prev_position + for prev_position in self._previous_positions + ) + or len(self._previous_angles) < 2 + or not all( + self._blind.angle == prev_angle for prev_angle in self._previous_angles + ) ): # keep updating the position @self._update_interval_moving until the position does not change. self._requesting_position = async_call_later( @@ -132,6 +143,7 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind ) else: self._previous_positions = [] + self._previous_angles = [] self._requesting_position = None async def async_request_position_till_stop(self, delay: int | None = None) -> None: @@ -140,7 +152,8 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind delay = self._update_interval_moving self._previous_positions = [] - if self._blind.position is None: + self._previous_angles = [] + if self._blind.position is None and self._blind.angle is None: return if self._requesting_position is not None: self._requesting_position() From 9f1cec893e57b73a1561078a7590b65ff9b952fb Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Fri, 1 Aug 2025 13:22:46 +0200 Subject: [PATCH 186/265] emoncms - fix missing data descriptions (#149733) --- homeassistant/components/emoncms/strings.json | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emoncms/strings.json b/homeassistant/components/emoncms/strings.json index 900e8dd0474..f68ea92d26c 100644 --- a/homeassistant/components/emoncms/strings.json +++ b/homeassistant/components/emoncms/strings.json @@ -12,12 +12,26 @@ }, "data_description": { "url": "Server URL starting with the protocol (http or https)", - "api_key": "Your 32 bits API key" + "api_key": "Your 32 bits API key", + "sync_mode": "Pick your feeds manually (default) or synchronize them at once" } }, "choose_feeds": { "data": { "include_only_feed_id": "Choose feeds to include" + }, + "data_description": { + "include_only_feed_id": "Pick the feeds you want to synchronize" + } + }, + "reconfigure": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "url": "[%key:component::emoncms::config::step::user::data_description::url%]", + "api_key": "[%key:component::emoncms::config::step::user::data_description::api_key%]" } } }, @@ -30,8 +44,8 @@ "selector": { "sync_mode": { "options": { - "auto": "Synchronize all available Feeds", - "manual": "Select which Feeds to synchronize" + "auto": "Synchronize all available feeds", + "manual": "Select which feeds to synchronize" } } }, @@ -89,6 +103,9 @@ "init": { "data": { "include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data::include_only_feed_id%]" + }, + "data_description": { + "include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data_description::include_only_feed_id%]" } } } From 3d4386ea6d4b5ec9f6a12ec3ee5fa3c4428b558f Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 14:32:14 +0200 Subject: [PATCH 187/265] Add translation for `absolute_humidity` device class to `random` (#149815) --- homeassistant/components/random/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index d57f2dc8eec..1f28000d0f4 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -82,6 +82,7 @@ }, "sensor_device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", From b5e4ae4a53196b07bb2cec3a869340ba96ad70a8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 1 Aug 2025 14:36:37 +0200 Subject: [PATCH 188/265] Improve Tado switch tests (#149810) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tado/snapshots/test_switch.ambr | 49 +++++++++++++++++++ tests/components/tado/test_switch.py | 29 ++++++++--- 2 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/components/tado/snapshots/test_switch.ambr diff --git a/tests/components/tado/snapshots/test_switch.ambr b/tests/components/tado/snapshots/test_switch.ambr new file mode 100644 index 00000000000..c2f00649f1d --- /dev/null +++ b/tests/components/tado/snapshots/test_switch.ambr @@ -0,0 +1,49 @@ +# serializer version: 1 +# name: test_entities[switch.baseboard_heater_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.baseboard_heater_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': '1 1 child-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.baseboard_heater_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Baseboard Heater Child lock', + }), + 'context': , + 'entity_id': 'switch.baseboard_heater_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tado/test_switch.py b/tests/components/tado/test_switch.py index 2112f3a1ac7..6bfdf1283d1 100644 --- a/tests/components/tado/test_switch.py +++ b/tests/components/tado/test_switch.py @@ -1,28 +1,45 @@ -"""The sensor tests for the tado platform.""" +"""The switch tests for the tado platform.""" +from collections.abc import AsyncGenerator from unittest.mock import patch import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.components.tado import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration +from tests.common import MockConfigEntry, snapshot_platform + CHILD_LOCK_SWITCH_ENTITY = "switch.baseboard_heater_child_lock" -async def test_child_lock(hass: HomeAssistant) -> None: - """Test creation of child lock entity.""" +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.SWITCH]): + yield + + +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: + """Test creation of switch entities.""" await async_init_integration(hass) - state = hass.states.get(CHILD_LOCK_SWITCH_ENTITY) - assert state.state == STATE_OFF + + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) @pytest.mark.parametrize( From 5ce2729dc23e4fcdd0be80fe4128df60437747d7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 1 Aug 2025 14:36:57 +0200 Subject: [PATCH 189/265] Improve Tado sensor tests (#149809) --- .../tado/snapshots/test_sensor.ambr | 1240 +++++++++++++++++ tests/components/tado/test_sensor.py | 75 +- 2 files changed, 1264 insertions(+), 51 deletions(-) create mode 100644 tests/components/tado/snapshots/test_sensor.ambr diff --git a/tests/components/tado/snapshots/test_sensor.ambr b/tests/components/tado/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..2040bd737c8 --- /dev/null +++ b/tests/components/tado/snapshots/test_sensor.ambr @@ -0,0 +1,1240 @@ +# serializer version: 1 +# name: test_entities[sensor.air_conditioning_ac-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.air_conditioning_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac', + 'unique_id': 'ac 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning AC', + 'time': '2020-03-05T04:01:07.162Z', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ON', + }) +# --- +# name: test_entities[sensor.air_conditioning_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'humidity 3 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.air_conditioning_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Air Conditioning Humidity', + 'state_class': , + 'time': '2020-03-05T03:57:38.850Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.9', + }) +# --- +# name: test_entities[sensor.air_conditioning_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning Tado mode', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.air_conditioning_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature 3 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.air_conditioning_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Air Conditioning Temperature', + 'setting': 0, + 'state_class': , + 'time': '2020-03-05T03:57:38.850Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.76', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_ac-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.air_conditioning_with_fanlevel_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac', + 'unique_id': 'ac 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning with fanlevel AC', + 'time': '2022-07-13T18: 06: 58.183Z', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_fanlevel_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ON', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_fanlevel_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'humidity 6 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Air Conditioning with fanlevel Humidity', + 'state_class': , + 'time': '2024-06-28T22: 23: 15.679Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_fanlevel_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '70.9', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_fanlevel_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning with fanlevel Tado mode', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_fanlevel_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_fanlevel_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature 6 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Air Conditioning with fanlevel Temperature', + 'setting': 0, + 'state_class': , + 'time': '2024-06-28T22: 23: 15.679Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_fanlevel_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.3', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_ac-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.air_conditioning_with_swing_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac', + 'unique_id': 'ac 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning with swing AC', + 'time': '2020-03-27T23:02:22.260Z', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_swing_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ON', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_swing_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'humidity 5 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Air Conditioning with swing Humidity', + 'state_class': , + 'time': '2020-03-28T02:09:27.830Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_swing_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.3', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_swing_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning with swing Tado mode', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_swing_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_swing_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature 5 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Air Conditioning with swing Temperature', + 'setting': 0, + 'state_class': , + 'time': '2020-03-28T02:09:27.830Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_swing_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.88', + }) +# --- +# name: test_entities[sensor.baseboard_heater_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.baseboard_heater_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'heating', + 'unique_id': 'heating 1 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.baseboard_heater_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Baseboard Heater Heating', + 'state_class': , + 'time': '2020-03-10T07:47:45.978Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.baseboard_heater_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor.baseboard_heater_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.baseboard_heater_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'humidity 1 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.baseboard_heater_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Baseboard Heater Humidity', + 'state_class': , + 'time': '2020-03-10T07:44:11.947Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.baseboard_heater_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.2', + }) +# --- +# name: test_entities[sensor.baseboard_heater_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.baseboard_heater_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.baseboard_heater_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Baseboard Heater Tado mode', + }), + 'context': , + 'entity_id': 'sensor.baseboard_heater_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.baseboard_heater_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.baseboard_heater_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature 1 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.baseboard_heater_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Baseboard Heater Temperature', + 'setting': 0, + 'state_class': , + 'time': '2020-03-10T07:44:11.947Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.baseboard_heater_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.65', + }) +# --- +# name: test_entities[sensor.home_name_automatic_geofencing-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.home_name_automatic_geofencing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Automatic geofencing', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'automatic_geofencing', + 'unique_id': 'automatic geofencing 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.home_name_automatic_geofencing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Automatic geofencing', + }), + 'context': , + 'entity_id': 'sensor.home_name_automatic_geofencing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) +# --- +# name: test_entities[sensor.home_name_geofencing_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_geofencing_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Geofencing mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'geofencing_mode', + 'unique_id': 'geofencing mode 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.home_name_geofencing_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Geofencing mode', + }), + 'context': , + 'entity_id': 'sensor.home_name_geofencing_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Home (Auto)', + }) +# --- +# name: test_entities[sensor.home_name_outdoor_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_outdoor_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outdoor temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'outdoor_temperature', + 'unique_id': 'outdoor temperature 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.home_name_outdoor_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'home name Outdoor temperature', + 'state_class': , + 'time': '2020-12-22T08:13:13.652Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_name_outdoor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.46', + }) +# --- +# name: test_entities[sensor.home_name_solar_percentage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.home_name_solar_percentage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Solar percentage', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'solar_percentage', + 'unique_id': 'solar percentage 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.home_name_solar_percentage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Solar percentage', + 'state_class': , + 'time': '2020-12-22T08:13:13.652Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_name_solar_percentage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.1', + }) +# --- +# name: test_entities[sensor.home_name_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.home_name_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Tado mode', + }), + 'context': , + 'entity_id': 'sensor.home_name_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.home_name_weather_condition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_weather_condition', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Weather condition', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'weather_condition', + 'unique_id': 'weather condition 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.home_name_weather_condition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Weather condition', + 'time': '2020-12-22T08:13:13.652Z', + }), + 'context': , + 'entity_id': 'sensor.home_name_weather_condition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'fog', + }) +# --- +# name: test_entities[sensor.second_water_heater_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.second_water_heater_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.second_water_heater_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Second Water Heater Tado mode', + }), + 'context': , + 'entity_id': 'sensor.second_water_heater_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.water_heater_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_heater_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.water_heater_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Heater Tado mode', + }), + 'context': , + 'entity_id': 'sensor.water_heater_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py index 0fa7a9ca370..8445683d11d 100644 --- a/tests/components/tado/test_sensor.py +++ b/tests/components/tado/test_sensor.py @@ -1,62 +1,35 @@ """The sensor tests for the tado platform.""" +from collections.abc import AsyncGenerator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.tado import DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration +from tests.common import MockConfigEntry, snapshot_platform -async def test_air_con_create_sensors(hass: HomeAssistant) -> None: - """Test creation of aircon sensors.""" + +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.SENSOR]): + yield + + +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: + """Test creation of sensor entities.""" await async_init_integration(hass) - state = hass.states.get("sensor.air_conditioning_tado_mode") - assert state.state == "HOME" + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - state = hass.states.get("sensor.air_conditioning_temperature") - assert state.state == "24.76" - - state = hass.states.get("sensor.air_conditioning_ac") - assert state.state == "ON" - - state = hass.states.get("sensor.air_conditioning_humidity") - assert state.state == "60.9" - - -async def test_home_create_sensors(hass: HomeAssistant) -> None: - """Test creation of home sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("sensor.home_name_outdoor_temperature") - assert state.state == "7.46" - - state = hass.states.get("sensor.home_name_solar_percentage") - assert state.state == "2.1" - - state = hass.states.get("sensor.home_name_weather_condition") - assert state.state == "fog" - - -async def test_heater_create_sensors(hass: HomeAssistant) -> None: - """Test creation of heater sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("sensor.baseboard_heater_tado_mode") - assert state.state == "HOME" - - state = hass.states.get("sensor.baseboard_heater_temperature") - assert state.state == "20.65" - - state = hass.states.get("sensor.baseboard_heater_humidity") - assert state.state == "45.2" - - -async def test_water_heater_create_sensors(hass: HomeAssistant) -> None: - """Test creation of water heater sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("sensor.water_heater_tado_mode") - assert state.state == "HOME" + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) From 37579440e659aed7d6ec1a04edae2e69840fdab5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 1 Aug 2025 14:37:12 +0200 Subject: [PATCH 190/265] Improve Tado climate tests (#149808) --- .../tado/snapshots/test_climate.ambr | 423 ++++++++++++++++++ tests/components/tado/test_climate.py | 126 +----- 2 files changed, 439 insertions(+), 110 deletions(-) diff --git a/tests/components/tado/snapshots/test_climate.ambr b/tests/components/tado/snapshots/test_climate.ambr index 6ba35b6f6f2..fb1dd6d46d1 100644 --- a/tests/components/tado/snapshots/test_climate.ambr +++ b/tests/components/tado/snapshots/test_climate.ambr @@ -93,6 +93,429 @@ }), ) # --- +# name: test_entities[climate.air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'target_temp_step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'tado', + 'unique_id': 'AIR_CONDITIONING 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 60.9, + 'current_temperature': 24.8, + 'default_overlay_seconds': None, + 'default_overlay_type': 'MANUAL', + 'fan_mode': 'auto', + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'friendly_name': 'Air Conditioning', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'offset_celsius': -1.0, + 'offset_fahrenheit': -1.8, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'supported_features': , + 'target_temp_step': 1, + 'temperature': 17.8, + }), + 'context': , + 'entity_id': 'climate.air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_entities[climate.air_conditioning_with_fanlevel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'high', + 'medium', + 'auto', + 'low', + ]), + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'swing_modes': list([ + 'vertical', + 'horizontal', + 'both', + 'off', + ]), + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.air_conditioning_with_fanlevel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'tado', + 'unique_id': 'AIR_CONDITIONING 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.air_conditioning_with_fanlevel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 70.9, + 'current_temperature': 24.3, + 'default_overlay_seconds': None, + 'default_overlay_type': 'MANUAL', + 'fan_mode': 'high', + 'fan_modes': list([ + 'high', + 'medium', + 'auto', + 'low', + ]), + 'friendly_name': 'Air Conditioning with fanlevel', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'supported_features': , + 'swing_mode': 'both', + 'swing_modes': list([ + 'vertical', + 'horizontal', + 'both', + 'off', + ]), + 'target_temp_step': 1.0, + 'temperature': 25.0, + }), + 'context': , + 'entity_id': 'climate.air_conditioning_with_fanlevel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_entities[climate.air_conditioning_with_swing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 30.0, + 'min_temp': 16.0, + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'swing_modes': list([ + 'on', + 'off', + ]), + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.air_conditioning_with_swing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'tado', + 'unique_id': 'AIR_CONDITIONING 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.air_conditioning_with_swing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 42.3, + 'current_temperature': 20.9, + 'default_overlay_seconds': None, + 'default_overlay_type': 'MANUAL', + 'fan_mode': 'auto', + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'friendly_name': 'Air Conditioning with swing', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 30.0, + 'min_temp': 16.0, + 'offset_celsius': -1.0, + 'offset_fahrenheit': -1.8, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'supported_features': , + 'swing_mode': 'off', + 'swing_modes': list([ + 'on', + 'off', + ]), + 'target_temp_step': 1.0, + 'temperature': 20.0, + }), + 'context': , + 'entity_id': 'climate.air_conditioning_with_swing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_entities[climate.baseboard_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'target_temp_step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.baseboard_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'tado', + 'unique_id': 'HEATING 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.baseboard_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 45.2, + 'current_temperature': 20.6, + 'default_overlay_seconds': None, + 'default_overlay_type': 'MANUAL', + 'friendly_name': 'Baseboard Heater', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'offset_celsius': -1.0, + 'offset_fahrenheit': -1.8, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'supported_features': , + 'target_temp_step': 1, + 'temperature': 20.5, + }), + 'context': , + 'entity_id': 'climate.baseboard_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- # name: test_heater_set_temperature _Call( tuple( diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 0699551c9c0..71ee0471e5f 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -1,5 +1,6 @@ -"""The sensor tests for the tado platform.""" +"""The climate tests for the tado platform.""" +from collections.abc import AsyncGenerator from unittest.mock import patch from PyTado.interface.api.my_tado import TadoZone @@ -13,128 +14,33 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.components.tado import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration - -async def test_air_con(hass: HomeAssistant) -> None: - """Test creation of aircon climate.""" - - await async_init_integration(hass) - - state = hass.states.get("climate.air_conditioning") - assert state.state == "cool" - - expected_attributes = { - "current_humidity": 60.9, - "current_temperature": 24.8, - "fan_mode": "auto", - "fan_modes": ["auto", "high", "medium", "low"], - "friendly_name": "Air Conditioning", - "hvac_action": "cooling", - "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], - "max_temp": 31.0, - "min_temp": 16.0, - "preset_mode": "auto", - "preset_modes": ["away", "home", "auto"], - "supported_features": 409, - "target_temp_step": 1, - "temperature": 17.8, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) +from tests.common import MockConfigEntry, snapshot_platform -async def test_heater(hass: HomeAssistant) -> None: - """Test creation of heater climate.""" - - await async_init_integration(hass) - - state = hass.states.get("climate.baseboard_heater") - assert state.state == "heat" - - expected_attributes = { - "current_humidity": 45.2, - "current_temperature": 20.6, - "friendly_name": "Baseboard Heater", - "hvac_action": "idle", - "hvac_modes": ["off", "auto", "heat"], - "max_temp": 31.0, - "min_temp": 16.0, - "preset_mode": "auto", - "preset_modes": ["away", "home", "auto"], - "supported_features": 401, - "target_temp_step": 1, - "temperature": 20.5, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.CLIMATE]): + yield -async def test_smartac_with_swing(hass: HomeAssistant) -> None: - """Test creation of smart ac with swing climate.""" - - await async_init_integration(hass) - - state = hass.states.get("climate.air_conditioning_with_swing") - assert state.state == "auto" - - expected_attributes = { - "current_humidity": 42.3, - "current_temperature": 20.9, - "fan_mode": "auto", - "fan_modes": ["auto", "high", "medium", "low"], - "friendly_name": "Air Conditioning with swing", - "hvac_action": "heating", - "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], - "max_temp": 30.0, - "min_temp": 16.0, - "preset_mode": "auto", - "preset_modes": ["away", "home", "auto"], - "swing_modes": ["on", "off"], - "supported_features": 441, - "target_temp_step": 1.0, - "temperature": 20.0, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) - - -async def test_smartac_with_fanlevel_vertical_and_horizontal_swing( - hass: HomeAssistant, +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: - """Test creation of smart ac with swing climate.""" + """Test creation of climate entities.""" await async_init_integration(hass) - state = hass.states.get("climate.air_conditioning_with_fanlevel") - assert state.state == "heat" + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - expected_attributes = { - "current_humidity": 70.9, - "current_temperature": 24.3, - "fan_mode": "high", - "fan_modes": ["high", "medium", "auto", "low"], - "friendly_name": "Air Conditioning with fanlevel", - "hvac_action": "heating", - "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], - "max_temp": 31.0, - "min_temp": 16.0, - "preset_mode": "auto", - "preset_modes": ["away", "home", "auto"], - "swing_modes": ["vertical", "horizontal", "both", "off"], - "supported_features": 441, - "target_temp_step": 1.0, - "temperature": 25.0, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) async def test_heater_set_temperature( From 506431c75fb9ceb0bb142fc6e0b07949828b178c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 1 Aug 2025 14:38:02 +0200 Subject: [PATCH 191/265] Improve Tado water heater tests (#149806) --- .../tado/snapshots/test_water_heater.ambr | 139 ++++++++++++++++++ tests/components/tado/test_water_heater.py | 62 +++----- 2 files changed, 163 insertions(+), 38 deletions(-) create mode 100644 tests/components/tado/snapshots/test_water_heater.ambr diff --git a/tests/components/tado/snapshots/test_water_heater.ambr b/tests/components/tado/snapshots/test_water_heater.ambr new file mode 100644 index 00000000000..5e10af60c8d --- /dev/null +++ b/tests/components/tado/snapshots/test_water_heater.ambr @@ -0,0 +1,139 @@ +# serializer version: 1 +# name: test_entities[water_heater.second_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 31.0, + 'min_temp': 16.0, + 'operation_list': list([ + 'auto', + 'heat', + 'off', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.second_water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[water_heater.second_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Second Water Heater', + 'max_temp': 31.0, + 'min_temp': 16.0, + 'operation_list': list([ + 'auto', + 'heat', + 'off', + ]), + 'operation_mode': 'heat', + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 30.0, + }), + 'context': , + 'entity_id': 'water_heater.second_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_entities[water_heater.water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 31.0, + 'min_temp': 16.0, + 'operation_list': list([ + 'auto', + 'heat', + 'off', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[water_heater.water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Water Heater', + 'max_temp': 31.0, + 'min_temp': 16.0, + 'operation_list': list([ + 'auto', + 'heat', + 'off', + ]), + 'operation_mode': 'auto', + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 65.0, + }), + 'context': , + 'entity_id': 'water_heater.water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'auto', + }) +# --- diff --git a/tests/components/tado/test_water_heater.py b/tests/components/tado/test_water_heater.py index 223a1fda16a..7c13ba1604e 100644 --- a/tests/components/tado/test_water_heater.py +++ b/tests/components/tado/test_water_heater.py @@ -1,49 +1,35 @@ -"""The sensor tests for the tado platform.""" +"""The water heater tests for the tado platform.""" +from collections.abc import AsyncGenerator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.tado import DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration +from tests.common import MockConfigEntry, snapshot_platform -async def test_water_heater_create_sensors(hass: HomeAssistant) -> None: + +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.WATER_HEATER]): + yield + + +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test creation of water heater.""" await async_init_integration(hass) - state = hass.states.get("water_heater.water_heater") - assert state.state == "auto" + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - expected_attributes = { - "current_temperature": None, - "friendly_name": "Water Heater", - "max_temp": 31.0, - "min_temp": 16.0, - "operation_list": ["auto", "heat", "off"], - "operation_mode": "auto", - "supported_features": 3, - "target_temp_high": None, - "target_temp_low": None, - "temperature": 65.0, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) - - state = hass.states.get("water_heater.second_water_heater") - assert state.state == "heat" - - expected_attributes = { - "current_temperature": None, - "friendly_name": "Second Water Heater", - "max_temp": 31.0, - "min_temp": 16.0, - "operation_list": ["auto", "heat", "off"], - "operation_mode": "heat", - "supported_features": 3, - "target_temp_high": None, - "target_temp_low": None, - "temperature": 30.0, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) From a08c3c9f442e055e740022ad8f604725b4cc5c98 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 1 Aug 2025 14:38:12 +0200 Subject: [PATCH 192/265] Improve Tado binary sensor tests (#149807) --- .../tado/snapshots/test_binary_sensor.ambr | 1230 +++++++++++++++++ tests/components/tado/test_binary_sensor.py | 84 +- 2 files changed, 1255 insertions(+), 59 deletions(-) create mode 100644 tests/components/tado/snapshots/test_binary_sensor.ambr diff --git a/tests/components/tado/snapshots/test_binary_sensor.ambr b/tests/components/tado/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..5920e6bbf11 --- /dev/null +++ b/tests/components/tado/snapshots/test_binary_sensor.ambr @@ -0,0 +1,1230 @@ +# serializer version: 1 +# name: test_entities[binary_sensor.air_conditioning_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Air Conditioning Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning Overlay', + 'termination': 'TADO_MODE', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning Power', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'open window 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Air Conditioning Window', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Air Conditioning with fanlevel Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning with fanlevel Overlay', + 'termination': 'MANUAL', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning with fanlevel Power', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'open window 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Air Conditioning with fanlevel Window', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_swing_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Air Conditioning with swing Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_swing_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_swing_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning with swing Overlay', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_swing_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_swing_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning with swing Power', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_swing_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_swing_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'open window 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Air Conditioning with swing Window', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_swing_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Baseboard Heater Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_early_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_early_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Early start', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'early_start', + 'unique_id': 'early start 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_early_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Baseboard Heater Early start', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_early_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Baseboard Heater Overlay', + 'termination': 'MANUAL', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Baseboard Heater Power', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'open window 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Baseboard Heater Window', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.second_water_heater_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Second Water Heater Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.second_water_heater_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.second_water_heater_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Second Water Heater Overlay', + 'termination': 'TADO_MODE', + }), + 'context': , + 'entity_id': 'binary_sensor.second_water_heater_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.second_water_heater_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Second Water Heater Power', + }), + 'context': , + 'entity_id': 'binary_sensor.second_water_heater_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.water_heater_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.water_heater_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.water_heater_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Water Heater Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.water_heater_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.water_heater_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.water_heater_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.water_heater_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Water Heater Overlay', + }), + 'context': , + 'entity_id': 'binary_sensor.water_heater_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.water_heater_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.water_heater_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.water_heater_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Water Heater Power', + }), + 'context': , + 'entity_id': 'binary_sensor.water_heater_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.wr1_connection_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.wr1_connection_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connection state', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'connection_state', + 'unique_id': 'connection state WR1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.wr1_connection_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'WR1 Connection state', + }), + 'context': , + 'entity_id': 'binary_sensor.wr1_connection_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.wr4_connection_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.wr4_connection_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connection state', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'connection_state', + 'unique_id': 'connection state WR4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.wr4_connection_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'WR4 Connection state', + }), + 'context': , + 'entity_id': 'binary_sensor.wr4_connection_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/tado/test_binary_sensor.py b/tests/components/tado/test_binary_sensor.py index 78cd91c56c6..9a0b94883fa 100644 --- a/tests/components/tado/test_binary_sensor.py +++ b/tests/components/tado/test_binary_sensor.py @@ -1,69 +1,35 @@ -"""The sensor tests for the tado platform.""" +"""The binary sensor tests for the tado platform.""" -from homeassistant.const import STATE_OFF, STATE_ON +from collections.abc import AsyncGenerator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.tado import DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration +from tests.common import MockConfigEntry, snapshot_platform -async def test_air_con_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of aircon sensors.""" + +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.BINARY_SENSOR]): + yield + + +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: + """Test creation of binary sensor.""" await async_init_integration(hass) - state = hass.states.get("binary_sensor.air_conditioning_power") - assert state.state == STATE_ON + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - state = hass.states.get("binary_sensor.air_conditioning_connectivity") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.air_conditioning_overlay") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.air_conditioning_window") - assert state.state == STATE_OFF - - -async def test_heater_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of heater sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("binary_sensor.baseboard_heater_power") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.baseboard_heater_connectivity") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.baseboard_heater_early_start") - assert state.state == STATE_OFF - - state = hass.states.get("binary_sensor.baseboard_heater_overlay") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.baseboard_heater_window") - assert state.state == STATE_OFF - - -async def test_water_heater_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of water heater sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("binary_sensor.water_heater_connectivity") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.water_heater_overlay") - assert state.state == STATE_OFF - - state = hass.states.get("binary_sensor.water_heater_power") - assert state.state == STATE_ON - - -async def test_home_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of home binary sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("binary_sensor.wr1_connection_state") - assert state.state == STATE_ON + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) From f538807d6eb522d2389ad040493780ed88f22516 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Aug 2025 14:54:58 +0200 Subject: [PATCH 193/265] Make device suggested_area only influence new devices (#149758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- homeassistant/helpers/device_registry.py | 43 +++++++++++------- tests/helpers/test_device_registry.py | 57 +++++++++++++++++------- 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index d3866d8c9c3..72d0cf651f2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -906,7 +906,19 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if device is None: deleted_device = self.deleted_devices.get_entry(identifiers, connections) if deleted_device is None: - device = DeviceEntry(is_new=True) + area_id: str | None = None + if ( + suggested_area is not None + and suggested_area is not UNDEFINED + and suggested_area != "" + ): + # Circular dep + from . import area_registry as ar # noqa: PLC0415 + + area = ar.async_get(self.hass).async_get_or_create(suggested_area) + area_id = area.id + device = DeviceEntry(is_new=True, area_id=area_id) + else: self.deleted_devices.pop(deleted_device.id) device = deleted_device.to_device_entry( @@ -961,7 +973,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): model_id=model_id, name=name, serial_number=serial_number, - suggested_area=suggested_area, + _suggested_area=suggested_area, sw_version=sw_version, via_device_id=via_device_id, ) @@ -1000,6 +1012,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): remove_config_entry_id: str | UndefinedType = UNDEFINED, remove_config_subentry_id: str | None | UndefinedType = UNDEFINED, serial_number: str | None | UndefinedType = UNDEFINED, + # _suggested_area is used internally by the device registry and must + # not be set by integrations. + _suggested_area: str | None | UndefinedType = UNDEFINED, + # suggested_area is deprecated and will be removed in 2026.9 suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, @@ -1065,19 +1081,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): "Cannot define both merge_identifiers and new_identifiers" ) - if ( - suggested_area is not None - and suggested_area is not UNDEFINED - and suggested_area != "" - and area_id is UNDEFINED - and old.area_id is None - ): - # Circular dep - from . import area_registry as ar # noqa: PLC0415 - - area = ar.async_get(self.hass).async_get_or_create(suggested_area) - area_id = area.id - if add_config_entry_id is not UNDEFINED: if add_config_subentry_id is UNDEFINED: # Interpret not specifying a subentry as None (the main entry) @@ -1155,6 +1158,16 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values["config_entries_subentries"] = config_entries_subentries old_values["config_entries_subentries"] = old.config_entries_subentries + if suggested_area is not UNDEFINED: + report_usage( + "passes a suggested_area to device_registry.async_update device", + core_behavior=ReportBehavior.LOG, + breaks_in_ha_version="2026.9.0", + ) + + if _suggested_area is not UNDEFINED: + suggested_area = _suggested_area + added_connections: set[tuple[str, str]] | None = None added_identifiers: set[tuple[str, str]] | None = None diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 4247da296fd..d056c25fc3b 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3210,20 +3210,35 @@ async def test_update_remove_config_subentries( } +@pytest.mark.parametrize( + ("initial_area", "device_area_id", "number_of_areas"), + [ + (None, None, 0), + ("Living Room", "living_room", 1), + ], +) async def test_update_suggested_area( hass: HomeAssistant, device_registry: dr.DeviceRegistry, area_registry: ar.AreaRegistry, mock_config_entry: MockConfigEntry, + initial_area: str | None, + device_area_id: str | None, + number_of_areas: int, ) -> None: - """Verify that we can update the suggested area version of a device.""" + """Verify that we can update the suggested area of a device. + + Updating the suggested area of a device should not create a new area, nor should + it change the area_id of the device. + """ update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) entry = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bla", "123")}, + suggested_area=initial_area, ) - assert entry.area_id is None + assert entry.area_id == device_area_id suggested_area = "Pool" @@ -3232,26 +3247,24 @@ async def test_update_suggested_area( entry.id, suggested_area=suggested_area ) - assert mock_save.call_count == 1 + # Check the device registry was not saved + assert mock_save.call_count == 0 assert updated_entry != entry + assert updated_entry.area_id == device_area_id - pool_area = area_registry.async_get_area_by_name("Pool") - assert pool_area is not None - assert updated_entry.area_id == pool_area.id - assert len(area_registry.areas) == 1 + # Check we did not create an area + pool_area = area_registry.async_get_area_by_name(suggested_area) + assert pool_area is None + assert updated_entry.area_id == device_area_id + assert len(area_registry.areas) == number_of_areas await hass.async_block_till_done() - assert len(update_events) == 2 + assert len(update_events) == 1 assert update_events[0].data == { "action": "create", "device_id": entry.id, } - assert update_events[1].data == { - "action": "update", - "device_id": entry.id, - "changes": {"area_id": None, "suggested_area": None}, - } # Do not save or fire the event if the suggested # area does not result in a change of area @@ -3260,10 +3273,10 @@ async def test_update_suggested_area( updated_entry = device_registry.async_update_device( entry.id, suggested_area="Other" ) - assert len(update_events) == 2 + assert len(update_events) == 1 assert mock_save_2.call_count == 0 assert updated_entry != entry - assert updated_entry.area_id == pool_area.id + assert updated_entry.area_id == device_area_id async def test_cleanup_device_registry( @@ -3397,11 +3410,13 @@ async def test_cleanup_entity_registry_change( assert len(mock_call.mock_calls) == 2 +@pytest.mark.parametrize("initial_area", [None, "12345A"]) @pytest.mark.usefixtures("freezer") async def test_restore_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_config_entry_with_subentries: MockConfigEntry, + initial_area: str | None, ) -> None: """Make sure device id is stable.""" entry_id = mock_config_entry_with_subentries.entry_id @@ -3428,7 +3443,7 @@ async def test_restore_device( # Apply user customizations entry = device_registry.async_update_device( entry.id, - area_id="12345A", + area_id=initial_area, disabled_by=dr.DeviceEntryDisabler.USER, labels={"label1", "label2"}, name_by_user="Test Friendly Name", @@ -3493,7 +3508,7 @@ async def test_restore_device( via_device="via_device_id_new", ) assert entry3 == dr.DeviceEntry( - area_id="12345A", + area_id=initial_area, config_entries={entry_id}, config_entries_subentries={entry_id: {subentry_id}}, configuration_url="http://config_url_new.bla", @@ -4933,3 +4948,11 @@ async def test_suggested_area_deprecation( "The deprecated function suggested_area was called. It will be removed in " "HA Core 2026.9. Use code which ignores suggested_area instead" ) in caplog.text + + device_registry.async_update_device(entry.id, suggested_area="TV Room") + + assert ( + "Detected code that passes a suggested_area to device_registry.async_update " + "device. This will stop working in Home Assistant 2026.9.0, please report " + "this issue" + ) in caplog.text From fb2d62d69242861aac77cec1043ac1da09584c07 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 15:57:47 +0200 Subject: [PATCH 194/265] Add translation for `absolute_humidity` device class to `mqtt` (#149818) --- homeassistant/components/mqtt/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 40215b0f2c6..0e248cfd2d2 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -1104,6 +1104,7 @@ }, "device_class_sensor": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", From b4a4e218ec98479954e94dfc9d6f058faa86c015 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Fri, 1 Aug 2025 16:42:59 +0200 Subject: [PATCH 195/265] Add re-authentication to BSBLan (#146280) Co-authored-by: Norbert Rittel --- .../components/bsblan/config_flow.py | 187 ++++++++- .../components/bsblan/coordinator.py | 14 +- homeassistant/components/bsblan/strings.json | 15 +- tests/components/bsblan/test_config_flow.py | 370 +++++++++++++++++- tests/components/bsblan/test_init.py | 34 +- 5 files changed, 577 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index 6abfe57a4ae..1491322ae13 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Any -from bsblan import BSBLAN, BSBLANConfig, BSBLANError +from bsblan import BSBLAN, BSBLANAuthError, BSBLANConfig, BSBLANError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -45,7 +46,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): self.username = user_input.get(CONF_USERNAME) self.password = user_input.get(CONF_PASSWORD) - return await self._validate_and_create() + return await self._validate_and_create(user_input) async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo @@ -128,14 +129,29 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): self.username = user_input.get(CONF_USERNAME) self.password = user_input.get(CONF_PASSWORD) - return await self._validate_and_create(is_discovery=True) + return await self._validate_and_create(user_input, is_discovery=True) async def _validate_and_create( - self, is_discovery: bool = False + self, user_input: dict[str, Any], is_discovery: bool = False ) -> ConfigFlowResult: """Validate device connection and create entry.""" try: - await self._get_bsblan_info(is_discovery=is_discovery) + await self._get_bsblan_info() + except BSBLANAuthError: + if is_discovery: + return self.async_show_form( + step_id="discovery_confirm", + data_schema=vol.Schema( + { + vol.Optional(CONF_PASSKEY): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + } + ), + errors={"base": "invalid_auth"}, + description_placeholders={"host": str(self.host)}, + ) + return self._show_setup_form({"base": "invalid_auth"}, user_input) except BSBLANError: if is_discovery: return self.async_show_form( @@ -154,18 +170,145 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth flow.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauth confirmation flow.""" + existing_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert existing_entry + + if user_input is None: + # Preserve existing values as defaults + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PASSKEY, + default=existing_entry.data.get( + CONF_PASSKEY, vol.UNDEFINED + ), + ): str, + vol.Optional( + CONF_USERNAME, + default=existing_entry.data.get( + CONF_USERNAME, vol.UNDEFINED + ), + ): str, + vol.Optional( + CONF_PASSWORD, + default=vol.UNDEFINED, + ): str, + } + ), + ) + + # Use existing host and port, update auth credentials + self.host = existing_entry.data[CONF_HOST] + self.port = existing_entry.data[CONF_PORT] + self.passkey = user_input.get(CONF_PASSKEY) or existing_entry.data.get( + CONF_PASSKEY + ) + self.username = user_input.get(CONF_USERNAME) or existing_entry.data.get( + CONF_USERNAME + ) + self.password = user_input.get(CONF_PASSWORD) + + try: + await self._get_bsblan_info(raise_on_progress=False, is_reauth=True) + except BSBLANAuthError: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PASSKEY, + default=user_input.get(CONF_PASSKEY, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_PASSWORD, + default=vol.UNDEFINED, + ): str, + } + ), + errors={"base": "invalid_auth"}, + ) + except BSBLANError: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PASSKEY, + default=user_input.get(CONF_PASSKEY, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_PASSWORD, + default=vol.UNDEFINED, + ): str, + } + ), + errors={"base": "cannot_connect"}, + ) + + # Update the config entry with new auth data + data_updates = {} + if self.passkey is not None: + data_updates[CONF_PASSKEY] = self.passkey + if self.username is not None: + data_updates[CONF_USERNAME] = self.username + if self.password is not None: + data_updates[CONF_PASSWORD] = self.password + + return self.async_update_reload_and_abort( + existing_entry, data_updates=data_updates, reason="reauth_successful" + ) + @callback - def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult: + def _show_setup_form( + self, errors: dict | None = None, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Show the setup form to the user.""" + # Preserve user input if provided, otherwise use defaults + defaults = user_input or {} + return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - vol.Optional(CONF_PASSKEY): str, - vol.Optional(CONF_USERNAME): str, - vol.Optional(CONF_PASSWORD): str, + vol.Required( + CONF_HOST, default=defaults.get(CONF_HOST, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_PORT, default=defaults.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Optional( + CONF_PASSKEY, default=defaults.get(CONF_PASSKEY, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_USERNAME, + default=defaults.get(CONF_USERNAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_PASSWORD, + default=defaults.get(CONF_PASSWORD, vol.UNDEFINED), + ): str, } ), errors=errors or {}, @@ -186,7 +329,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): ) async def _get_bsblan_info( - self, raise_on_progress: bool = True, is_discovery: bool = False + self, + raise_on_progress: bool = True, + is_reauth: bool = False, ) -> None: """Get device information from a BSBLAN device.""" config = BSBLANConfig( @@ -209,11 +354,13 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): format_mac(self.mac), raise_on_progress=raise_on_progress ) - # Always allow updating host/port for both user and discovery flows - # This ensures connectivity is maintained when devices change IP addresses - self._abort_if_unique_id_configured( - updates={ - CONF_HOST: self.host, - CONF_PORT: self.port, - } - ) + # Skip unique_id configuration check during reauth to prevent "already_configured" abort + if not is_reauth: + # Always allow updating host/port for both user and discovery flows + # This ensures connectivity is maintained when devices change IP addresses + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self.host, + CONF_PORT: self.port, + } + ) diff --git a/homeassistant/components/bsblan/coordinator.py b/homeassistant/components/bsblan/coordinator.py index 5c5e23efa8a..38a19dba8ea 100644 --- a/homeassistant/components/bsblan/coordinator.py +++ b/homeassistant/components/bsblan/coordinator.py @@ -4,11 +4,19 @@ from dataclasses import dataclass from datetime import timedelta from random import randint -from bsblan import BSBLAN, BSBLANConnectionError, HotWaterState, Sensor, State +from bsblan import ( + BSBLAN, + BSBLANAuthError, + BSBLANConnectionError, + HotWaterState, + Sensor, + State, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER, SCAN_INTERVAL @@ -62,6 +70,10 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]): state = await self.client.state() sensor = await self.client.sensor() dhw = await self.client.hot_water_state() + except BSBLANAuthError as err: + raise ConfigEntryAuthFailed( + "Authentication failed for BSB-Lan device" + ) from err except BSBLANConnectionError as err: host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown" raise UpdateFailed( diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json index cd4633dfb86..86e52e76f41 100644 --- a/homeassistant/components/bsblan/strings.json +++ b/homeassistant/components/bsblan/strings.json @@ -33,14 +33,25 @@ "username": "[%key:component::bsblan::config::step::user::data_description::username%]", "password": "[%key:component::bsblan::config::step::user::data_description::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The BSB-Lan integration needs to re-authenticate with {name}", + "data": { + "passkey": "[%key:component::bsblan::config::step::user::data::passkey%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "exceptions": { diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 72360ece687..3ca0de5b78f 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -3,11 +3,11 @@ from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock -from bsblan import BSBLANConnectionError +from bsblan import BSBLANAuthError, BSBLANConnectionError, BSBLANError import pytest from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -129,7 +129,7 @@ async def test_full_user_flow_implementation( result = await _init_user_flow(hass) _assert_form_result(result, "user") - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -142,7 +142,7 @@ async def test_full_user_flow_implementation( ) _assert_create_entry_result( - result2, + result, format_mac("00:80:41:19:69:90"), { CONF_HOST: "127.0.0.1", @@ -185,6 +185,94 @@ async def test_connection_error( _assert_form_result(result, "user", {"base": "cannot_connect"}) +async def test_authentication_error( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test we show user form on BSBLan authentication error with field preservation.""" + mock_bsblan.device.side_effect = BSBLANAuthError + + user_input = { + CONF_HOST: "192.168.1.100", + CONF_PORT: 8080, + CONF_PASSKEY: "secret", + CONF_USERNAME: "testuser", + CONF_PASSWORD: "wrongpassword", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result.get("type") is FlowResultType.FORM + assert result.get("errors") == {"base": "invalid_auth"} + assert result.get("step_id") == "user" + + # Verify that user input is preserved in the form + data_schema = result.get("data_schema") + assert data_schema is not None + + # Check that the form fields contain the previously entered values + host_field = next( + field for field in data_schema.schema if field.schema == CONF_HOST + ) + port_field = next( + field for field in data_schema.schema if field.schema == CONF_PORT + ) + passkey_field = next( + field for field in data_schema.schema if field.schema == CONF_PASSKEY + ) + username_field = next( + field for field in data_schema.schema if field.schema == CONF_USERNAME + ) + password_field = next( + field for field in data_schema.schema if field.schema == CONF_PASSWORD + ) + + # The defaults are callable functions, so we need to call them + assert host_field.default() == "192.168.1.100" + assert port_field.default() == 8080 + assert passkey_field.default() == "secret" + assert username_field.default() == "testuser" + assert password_field.default() == "wrongpassword" + + +async def test_authentication_error_vs_connection_error( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test that authentication and connection errors are handled differently.""" + # Test connection error first + mock_bsblan.device.side_effect = BSBLANConnectionError + + result = await _init_user_flow( + hass, + { + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + }, + ) + + _assert_form_result(result, "user", {"base": "cannot_connect"}) + + # Reset and test authentication error + mock_bsblan.device.side_effect = BSBLANAuthError + + result = await _init_user_flow( + hass, + { + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + CONF_USERNAME: "admin", + CONF_PASSWORD: "wrongpass", + }, + ) + + _assert_form_result(result, "user", {"base": "invalid_auth"}) + + async def test_user_device_exists_abort( hass: HomeAssistant, mock_bsblan: MagicMock, @@ -217,7 +305,7 @@ async def test_zeroconf_discovery( result = await _init_zeroconf_flow(hass, zeroconf_discovery_info) _assert_form_result(result, "discovery_confirm") - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -228,7 +316,7 @@ async def test_zeroconf_discovery( ) _assert_create_entry_result( - result2, + result, format_mac("00:80:41:19:69:90"), { CONF_HOST: "10.0.2.60", @@ -285,7 +373,7 @@ async def test_zeroconf_discovery_no_mac_requires_auth( # Reset side_effect for the second call to succeed mock_bsblan.device.side_effect = None - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -295,7 +383,7 @@ async def test_zeroconf_discovery_no_mac_requires_auth( ) _assert_create_entry_result( - result2, + result, "00:80:41:19:69:90", # MAC from fixture file { CONF_HOST: "10.0.2.60", @@ -324,10 +412,10 @@ async def test_zeroconf_discovery_no_mac_no_auth_required( _assert_form_result(result, "discovery_confirm") # User confirms the discovery - result2 = await _configure_flow(hass, result["flow_id"], {}) + result = await _configure_flow(hass, result["flow_id"], {}) _assert_create_entry_result( - result2, + result, "00:80:41:19:69:90", # MAC from fixture file { CONF_HOST: "10.0.2.60", @@ -355,7 +443,7 @@ async def test_zeroconf_discovery_connection_error( result = await _init_zeroconf_flow(hass, zeroconf_discovery_info) _assert_form_result(result, "discovery_confirm") - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -365,7 +453,7 @@ async def test_zeroconf_discovery_connection_error( }, ) - _assert_form_result(result2, "discovery_confirm", {"base": "cannot_connect"}) + _assert_form_result(result, "discovery_confirm", {"base": "cannot_connect"}) async def test_zeroconf_discovery_updates_host_port_on_existing_entry( @@ -445,7 +533,7 @@ async def test_zeroconf_discovery_connection_error_recovery( result = await _init_zeroconf_flow(hass, zeroconf_discovery_info) _assert_form_result(result, "discovery_confirm") - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -455,12 +543,12 @@ async def test_zeroconf_discovery_connection_error_recovery( }, ) - _assert_form_result(result2, "discovery_confirm", {"base": "cannot_connect"}) + _assert_form_result(result, "discovery_confirm", {"base": "cannot_connect"}) # Second attempt succeeds (connection is fixed) mock_bsblan.device.side_effect = None - result3 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -471,7 +559,7 @@ async def test_zeroconf_discovery_connection_error_recovery( ) _assert_create_entry_result( - result3, + result, format_mac("00:80:41:19:69:90"), { CONF_HOST: "10.0.2.60", @@ -513,7 +601,7 @@ async def test_connection_error_recovery( # Second attempt succeeds (connection is fixed) mock_bsblan.device.side_effect = None - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -526,7 +614,7 @@ async def test_connection_error_recovery( ) _assert_create_entry_result( - result2, + result, format_mac("00:80:41:19:69:90"), { CONF_HOST: "127.0.0.1", @@ -567,3 +655,249 @@ async def test_zeroconf_discovery_no_mac_duplicate_host_port( # Should not call device API since we abort early assert len(mock_bsblan.device.mock_calls) == 0 + + +async def test_reauth_flow_success( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful reauth flow.""" + mock_config_entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + _assert_form_result(result, "reauth_confirm") + + # Check that the form has the correct description placeholder + assert result.get("description_placeholders") == {"name": "BSBLAN Setup"} + + # Check that existing values are preserved as defaults + data_schema = result.get("data_schema") + assert data_schema is not None + + # Complete reauth with new credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "new_passkey", + CONF_USERNAME: "new_admin", + CONF_PASSWORD: "new_password", + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify config entry was updated with new credentials + assert mock_config_entry.data[CONF_PASSKEY] == "new_passkey" + assert mock_config_entry.data[CONF_USERNAME] == "new_admin" + assert mock_config_entry.data[CONF_PASSWORD] == "new_password" + # Verify host and port remain unchanged + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert mock_config_entry.data[CONF_PORT] == 80 + + +async def test_reauth_flow_auth_error( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with authentication error.""" + mock_config_entry.add_to_hass(hass) + + # Mock authentication error + mock_bsblan.device.side_effect = BSBLANAuthError + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + _assert_form_result(result, "reauth_confirm") + + # Submit with wrong credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "wrong_passkey", + CONF_USERNAME: "wrong_admin", + CONF_PASSWORD: "wrong_password", + }, + ) + + _assert_form_result(result, "reauth_confirm", {"base": "invalid_auth"}) + + # Verify that user input is preserved in the form after error + data_schema = result.get("data_schema") + assert data_schema is not None + + # Check that the form fields contain the previously entered values + passkey_field = next( + field for field in data_schema.schema if field.schema == CONF_PASSKEY + ) + username_field = next( + field for field in data_schema.schema if field.schema == CONF_USERNAME + ) + + assert passkey_field.default() == "wrong_passkey" + assert username_field.default() == "wrong_admin" + + +async def test_reauth_flow_connection_error( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with connection error.""" + mock_config_entry.add_to_hass(hass) + + # Mock connection error + mock_bsblan.device.side_effect = BSBLANConnectionError + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + _assert_form_result(result, "reauth_confirm") + + # Submit credentials but get connection error + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "1234", + CONF_USERNAME: "admin", + CONF_PASSWORD: "admin1234", + }, + ) + + _assert_form_result(result, "reauth_confirm", {"base": "cannot_connect"}) + + +async def test_reauth_flow_preserves_existing_values( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that reauth flow preserves existing values when user doesn't change them.""" + mock_config_entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + _assert_form_result(result, "reauth_confirm") + + # Submit without changing any credentials (only password is provided) + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSWORD: "new_password_only", + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify that existing passkey and username are preserved + assert mock_config_entry.data[CONF_PASSKEY] == "1234" # Original value + assert mock_config_entry.data[CONF_USERNAME] == "admin" # Original value + assert mock_config_entry.data[CONF_PASSWORD] == "new_password_only" # New value + + +async def test_reauth_flow_partial_credentials_update( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with partial credential updates.""" + mock_config_entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + # Submit with only username and password changes + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_USERNAME: "new_admin", + CONF_PASSWORD: "new_password", + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify partial update: passkey preserved, username and password updated + assert mock_config_entry.data[CONF_PASSKEY] == "1234" # Original preserved + assert mock_config_entry.data[CONF_USERNAME] == "new_admin" # Updated + assert mock_config_entry.data[CONF_PASSWORD] == "new_password" # Updated + # Host and port should remain unchanged + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert mock_config_entry.data[CONF_PORT] == 80 + + +async def test_zeroconf_discovery_auth_error_during_confirm( + hass: HomeAssistant, + mock_bsblan: MagicMock, + zeroconf_discovery_info: ZeroconfServiceInfo, +) -> None: + """Test authentication error during discovery_confirm step.""" + # Remove MAC from discovery to force discovery_confirm step + zeroconf_discovery_info.properties.pop("mac", None) + + # Setup device to require authentication during initial discovery + mock_bsblan.device.side_effect = BSBLANError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf_discovery_info, + ) + + _assert_form_result(result, "discovery_confirm") + + # Now setup auth error for the confirmation step + mock_bsblan.device.side_effect = BSBLANAuthError + + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "wrong_key", + CONF_USERNAME: "admin", + CONF_PASSWORD: "wrong_password", + }, + ) + + # Should show the discovery_confirm form again with auth error + _assert_form_result(result, "discovery_confirm", {"base": "invalid_auth"}) diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py index a9c3605f67f..cc52799d28b 100644 --- a/tests/components/bsblan/test_init.py +++ b/tests/components/bsblan/test_init.py @@ -2,13 +2,14 @@ from unittest.mock import MagicMock -from bsblan import BSBLANConnectionError +from bsblan import BSBLANAuthError, BSBLANConnectionError +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.bsblan.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_load_unload_config_entry( @@ -45,3 +46,32 @@ async def test_config_entry_not_ready( assert len(mock_bsblan.state.mock_calls) == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_auth_failed_triggers_reauth( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_bsblan: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that BSBLANAuthError during coordinator update triggers reauth flow.""" + # First, set up the integration successfully + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + # Mock BSBLANAuthError during next update + mock_bsblan.initialize.side_effect = BSBLANAuthError("Authentication failed") + + # Advance time by the coordinator's update interval to trigger update + freezer.tick(delta=20) # Advance beyond the 12 second scan interval + random offset + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Check that a reauth flow has been started + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" + assert flows[0]["context"]["entry_id"] == mock_config_entry.entry_id From 8d68fee9f8cebb7586a79d8367425c5ed1f77d1a Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 19:30:59 +0200 Subject: [PATCH 196/265] Add translation for `absolute_humidity` device class to `template` (#149814) --- homeassistant/components/template/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index d29bfbeb3fb..fc889ff9f8f 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -901,6 +901,7 @@ }, "sensor_device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", From d43f21c2e238bb18d39ebcb29e2e810e8c04e4bc Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 20:35:48 +0200 Subject: [PATCH 197/265] Fix descriptions for template number fields (#149804) --- homeassistant/components/template/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index fc889ff9f8f..cdaeacbe842 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -278,10 +278,10 @@ "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", "state": "Template for the number's current value.", - "step": "Template for the number's increment/decrement step.", + "step": "Defines the number's increment/decrement step.", "set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.", - "max": "Template for the number's maximum value.", - "min": "Template for the number's minimum value.", + "max": "Defines the number's maximum value.", + "min": "Defines the number's minimum value.", "unit_of_measurement": "Defines the unit of measurement of the number, if any." }, "sections": { From 9394546668b444d28302671281b29cea664bdd30 Mon Sep 17 00:00:00 2001 From: kizovinh Date: Sat, 2 Aug 2025 02:00:53 +0700 Subject: [PATCH 198/265] Add EZVIZ battery camera power status and online status sensor (#146822) --- homeassistant/components/ezviz/sensor.py | 46 +++++++++++++++++---- homeassistant/components/ezviz/strings.json | 12 ++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index c441b34b42d..ec631e8e5c1 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -66,6 +66,26 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key="last_alarm_type_name", translation_key="last_alarm_type_name", ), + "Record_Mode": SensorEntityDescription( + key="Record_Mode", + translation_key="record_mode", + entity_registry_enabled_default=False, + ), + "battery_camera_work_mode": SensorEntityDescription( + key="battery_camera_work_mode", + translation_key="battery_camera_work_mode", + entity_registry_enabled_default=False, + ), + "powerStatus": SensorEntityDescription( + key="powerStatus", + translation_key="power_status", + entity_registry_enabled_default=False, + ), + "OnlineStatus": SensorEntityDescription( + key="OnlineStatus", + translation_key="online_status", + entity_registry_enabled_default=False, + ), } @@ -76,16 +96,26 @@ async def async_setup_entry( ) -> None: """Set up EZVIZ sensors based on a config entry.""" coordinator = entry.runtime_data + entities: list[EzvizSensor] = [] - async_add_entities( - [ + for camera, sensors in coordinator.data.items(): + entities.extend( EzvizSensor(coordinator, camera, sensor) - for camera in coordinator.data - for sensor, value in coordinator.data[camera].items() - if sensor in SENSOR_TYPES - if value is not None - ] - ) + for sensor, value in sensors.items() + if sensor in SENSOR_TYPES and value is not None + ) + + optionals = sensors.get("optionals", {}) + entities.extend( + EzvizSensor(coordinator, camera, optional_key) + for optional_key in ("powerStatus", "OnlineStatus") + if optional_key in optionals + ) + + if "mode" in optionals.get("Record_Mode", {}): + entities.append(EzvizSensor(coordinator, camera, "mode")) + + async_add_entities(entities) class EzvizSensor(EzvizEntity, SensorEntity): diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index b03a5dbc61a..ad8f7114407 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -147,6 +147,18 @@ }, "last_alarm_type_name": { "name": "Last alarm type name" + }, + "record_mode": { + "name": "Record mode" + }, + "battery_camera_work_mode": { + "name": "Battery work mode" + }, + "power_status": { + "name": "Power status" + }, + "online_status": { + "name": "Online status" } }, "switch": { From 9616c8cd7bec74f9096e93727a6a6fa6a2ec0757 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Fri, 1 Aug 2025 21:04:16 +0200 Subject: [PATCH 199/265] Bump pyemoncms to 0.1.2 (#149825) --- homeassistant/components/emoncms/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index c7f18cb205e..bc86e6e9bab 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emoncms", "iot_class": "local_polling", - "requirements": ["pyemoncms==0.1.1"] + "requirements": ["pyemoncms==0.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index baad7de1409..6af1d9cb285 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1963,7 +1963,7 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms -pyemoncms==0.1.1 +pyemoncms==0.1.2 # homeassistant.components.enphase_envoy pyenphase==2.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93709fec4b5..d7efdf66a44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms -pyemoncms==0.1.1 +pyemoncms==0.1.2 # homeassistant.components.enphase_envoy pyenphase==2.2.3 From ae42d71123a9f5732a1c664c29359b709df1285d Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 21:33:47 +0200 Subject: [PATCH 200/265] Add translations for recently introduced device classes to `sql` (#149821) --- homeassistant/components/sql/strings.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index f9b8044e992..cbc0deda96a 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -71,10 +71,13 @@ "selector": { "device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", + "area": "[%key:component::sensor::entity_component::area::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", + "blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", @@ -85,6 +88,7 @@ "distance": "[%key:component::sensor::entity_component::distance::name%]", "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", "gas": "[%key:component::sensor::entity_component::gas::name%]", @@ -115,13 +119,14 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]", "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, From 8562c8d32f7e8fff5cc332776beb77ef7f3b91c6 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 21:34:31 +0200 Subject: [PATCH 201/265] Add translations for recently introduced device classes to `scrape` (#149822) --- homeassistant/components/scrape/strings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index d46f63c9516..91452287ce7 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -139,6 +139,7 @@ "selector": { "device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", @@ -155,6 +156,7 @@ "distance": "[%key:component::sensor::entity_component::distance::name%]", "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", "gas": "[%key:component::sensor::entity_component::gas::name%]", @@ -184,13 +186,14 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]", "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, From d45c03a79515aef1e1d847bea02e1a33fabcff65 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 21:35:04 +0200 Subject: [PATCH 202/265] Update reference for `volatile_organic_compounds_parts` in `random` (#149832) --- homeassistant/components/random/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index 1f28000d0f4..450f78f9e83 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -130,7 +130,7 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", From b0e75e9ee47d515623007c3f68754337b37bc25c Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 1 Aug 2025 21:36:10 +0200 Subject: [PATCH 203/265] Update reference for `volatile_organic_compounds_parts` in `template` (#149831) --- homeassistant/components/template/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index cdaeacbe842..be5fb1866ea 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -949,7 +949,7 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", From bddd4d621a98f5ba1f0c0e46b01d148e645b504c Mon Sep 17 00:00:00 2001 From: Jamin Date: Fri, 1 Aug 2025 14:37:45 -0500 Subject: [PATCH 204/265] Bump VoIP utils to 0.3.4 (#149786) --- homeassistant/components/voip/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index 0b533795a2c..fe855159d55 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["voip_utils"], "quality_scale": "internal", - "requirements": ["voip-utils==0.3.3"] + "requirements": ["voip-utils==0.3.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6af1d9cb285..e381f0f4363 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3057,7 +3057,7 @@ venstarcolortouch==0.21 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.3 +voip-utils==0.3.4 # homeassistant.components.volkszaehler volkszaehler==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7efdf66a44..ee4a582885d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2525,7 +2525,7 @@ venstarcolortouch==0.21 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.3 +voip-utils==0.3.4 # homeassistant.components.volvo volvocarsapi==0.4.1 From 45f6778ff480add585ac2585d4a35f4c60e5f762 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Sat, 2 Aug 2025 18:37:57 +0200 Subject: [PATCH 205/265] Fix Miele hob translation keys (#149865) --- homeassistant/components/miele/strings.json | 42 ++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index a4400ff26eb..90689a3d9cc 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -203,27 +203,27 @@ "plate": { "name": "Plate {plate_no}", "state": { - "power_step_0": "0", - "power_step_warm": "Warming", - "power_step_1": "1", - "power_step_2": "1\u2022", - "power_step_3": "2", - "power_step_4": "2\u2022", - "power_step_5": "3", - "power_step_6": "3\u2022", - "power_step_7": "4", - "power_step_8": "4\u2022", - "power_step_9": "5", - "power_step_10": "5\u2022", - "power_step_11": "6", - "power_step_12": "6\u2022", - "power_step_13": "7", - "power_step_14": "7\u2022", - "power_step_15": "8", - "power_step_16": "8\u2022", - "power_step_17": "9", - "power_step_18": "9\u2022", - "power_step_boost": "Boost" + "plate_step_0": "0", + "plate_step_warm": "Warming", + "plate_step_1": "1", + "plate_step_2": "1\u2022", + "plate_step_3": "2", + "plate_step_4": "2\u2022", + "plate_step_5": "3", + "plate_step_6": "3\u2022", + "plate_step_7": "4", + "plate_step_8": "4\u2022", + "plate_step_9": "5", + "plate_step_10": "5\u2022", + "plate_step_11": "6", + "plate_step_12": "6\u2022", + "plate_step_13": "7", + "plate_step_14": "7\u2022", + "plate_step_15": "8", + "plate_step_16": "8\u2022", + "plate_step_17": "9", + "plate_step_18": "9\u2022", + "plate_step_boost": "Boost" } }, "drying_step": { From c0bf167e10b8aaa9f7205072e19b71e950c1bcc5 Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sat, 2 Aug 2025 19:44:01 +0200 Subject: [PATCH 206/265] Update `denonavr` to `1.1.2` (#149842) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index c5a1b9aeb63..8fea21b707e 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/denonavr", "iot_class": "local_push", "loggers": ["denonavr"], - "requirements": ["denonavr==1.1.1"], + "requirements": ["denonavr==1.1.2"], "ssdp": [ { "manufacturer": "Denon", diff --git a/requirements_all.txt b/requirements_all.txt index e381f0f4363..eede7860c51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -791,7 +791,7 @@ deluge-client==1.10.2 demetriek==1.3.0 # homeassistant.components.denonavr -denonavr==1.1.1 +denonavr==1.1.2 # homeassistant.components.devialet devialet==1.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee4a582885d..6ef659032dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -691,7 +691,7 @@ deluge-client==1.10.2 demetriek==1.3.0 # homeassistant.components.denonavr -denonavr==1.1.1 +denonavr==1.1.2 # homeassistant.components.devialet devialet==1.5.7 From 3e615fd373ac950b9d50a2eb15e49a171c50b14b Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Sat, 2 Aug 2025 19:51:08 +0200 Subject: [PATCH 207/265] Improve code quality for garage door modules in homematicip_cloud (#149856) --- .../components/homematicip_cloud/cover.py | 8 +-- .../homematicip_cloud/test_cover.py | 52 ++++++++++++------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 931b689fb08..e846a360d39 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -283,19 +283,19 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): @property def is_closed(self) -> bool | None: """Return if the cover is closed.""" - return self._device.doorState == DoorState.CLOSED + return self.functional_channel.doorState == DoorState.CLOSED async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self._device.send_door_command_async(DoorCommand.OPEN) + await self.functional_channel.async_send_door_command(DoorCommand.OPEN) async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self._device.send_door_command_async(DoorCommand.CLOSE) + await self.functional_channel.async_send_door_command(DoorCommand.CLOSE) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - await self._device.send_door_command_async(DoorCommand.STOP) + await self.functional_channel.async_send_door_command(DoorCommand.STOP) class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity): diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index b005090309b..9b152988c24 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -365,14 +365,16 @@ async def test_hmip_garage_door_tormatic( assert ha_state.state == "closed" assert ha_state.attributes["current_position"] == 0 - service_call_counter = len(hmip_device.mock_calls) + service_call_counter = len(hmip_device.functionalChannels[1].mock_calls) await hass.services.async_call( "cover", "open_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 1 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.OPEN,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 1 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.OPEN,) await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.OPEN) ha_state = hass.states.get(entity_id) assert ha_state.state == CoverState.OPEN @@ -381,9 +383,11 @@ async def test_hmip_garage_door_tormatic( await hass.services.async_call( "cover", "close_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 3 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.CLOSE,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 2 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.CLOSE,) await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.CLOSED) ha_state = hass.states.get(entity_id) assert ha_state.state == CoverState.CLOSED @@ -392,9 +396,11 @@ async def test_hmip_garage_door_tormatic( await hass.services.async_call( "cover", "stop_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 5 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.STOP,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 3 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.STOP,) async def test_hmip_garage_door_hoermann( @@ -414,14 +420,16 @@ async def test_hmip_garage_door_hoermann( assert ha_state.state == "closed" assert ha_state.attributes["current_position"] == 0 - service_call_counter = len(hmip_device.mock_calls) + service_call_counter = len(hmip_device.functionalChannels[1].mock_calls) await hass.services.async_call( "cover", "open_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 1 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.OPEN,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 1 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.OPEN,) await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.OPEN) ha_state = hass.states.get(entity_id) assert ha_state.state == CoverState.OPEN @@ -430,9 +438,11 @@ async def test_hmip_garage_door_hoermann( await hass.services.async_call( "cover", "close_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 3 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.CLOSE,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 2 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.CLOSE,) await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.CLOSED) ha_state = hass.states.get(entity_id) assert ha_state.state == CoverState.CLOSED @@ -441,9 +451,11 @@ async def test_hmip_garage_door_hoermann( await hass.services.async_call( "cover", "stop_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 5 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.STOP,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 3 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.STOP,) async def test_hmip_cover_shutter_group( From 7dd2b9e4221edb7f1b1e8d773abe73cd6d8d512c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 3 Aug 2025 03:54:19 +1000 Subject: [PATCH 208/265] Make history coordinator more reliable in Tesla Fleet (#149854) --- homeassistant/components/tesla_fleet/coordinator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py index 20d2d70b5dc..e3a31a2c0dc 100644 --- a/homeassistant/components/tesla_fleet/coordinator.py +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -247,11 +247,15 @@ class TeslaFleetEnergySiteHistoryCoordinator(DataUpdateCoordinator[dict[str, Any raise UpdateFailed(e.message) from e self.updated_once = True + if not data or not isinstance(data.get("time_series"), list): + raise UpdateFailed("Received invalid data") + # Add all time periods together output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0) for period in data.get("time_series", []): for key in ENERGY_HISTORY_FIELDS: - output[key] += period.get(key, 0) + if key in period: + output[key] += period[key] return output From 018197e41a5972fc6e0c21da633b7de7803e4696 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sat, 2 Aug 2025 19:55:45 +0200 Subject: [PATCH 209/265] Add notifiers to send direct messages to friends in PlayStation Network (#149844) --- .../components/playstation_network/notify.py | 95 +++++++++++++++---- .../playstation_network/strings.json | 3 + .../snapshots/test_notify.ambr | 49 ++++++++++ .../playstation_network/test_notify.py | 11 ++- 4 files changed, 134 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/playstation_network/notify.py b/homeassistant/components/playstation_network/notify.py index 872ad98a594..a06359ebffc 100644 --- a/homeassistant/components/playstation_network/notify.py +++ b/homeassistant/components/playstation_network/notify.py @@ -3,6 +3,7 @@ from __future__ import annotations from enum import StrEnum +from typing import TYPE_CHECKING from psnawp_api.core.psnawp_exceptions import ( PSNAWPClientError, @@ -10,12 +11,14 @@ from psnawp_api.core.psnawp_exceptions import ( PSNAWPNotFoundError, PSNAWPServerError, ) +from psnawp_api.models.group.group import Group from homeassistant.components.notify import ( DOMAIN as NOTIFY_DOMAIN, NotifyEntity, NotifyEntityDescription, ) +from homeassistant.config_entries import ConfigSubentry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -24,6 +27,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .coordinator import ( PlaystationNetworkConfigEntry, + PlaystationNetworkFriendDataCoordinator, PlaystationNetworkGroupsUpdateCoordinator, ) from .entity import PlaystationNetworkServiceEntity @@ -35,6 +39,7 @@ class PlaystationNetworkNotify(StrEnum): """PlayStation Network sensors.""" GROUP_MESSAGE = "group_message" + DIRECT_MESSAGE = "direct_message" async def async_setup_entry( @@ -45,6 +50,7 @@ async def async_setup_entry( """Set up the notify entity platform.""" coordinator = config_entry.runtime_data.groups + groups_added: set[str] = set() entity_registry = er.async_get(hass) @@ -72,8 +78,50 @@ async def async_setup_entry( coordinator.async_add_listener(add_entities) add_entities() + for subentry_id, friend_coordinator in config_entry.runtime_data.friends.items(): + async_add_entities( + [ + PlaystationNetworkDirectMessageNotifyEntity( + friend_coordinator, + config_entry.subentries[subentry_id], + ) + ], + config_subentry_id=subentry_id, + ) -class PlaystationNetworkNotifyEntity(PlaystationNetworkServiceEntity, NotifyEntity): + +class PlaystationNetworkNotifyBaseEntity(PlaystationNetworkServiceEntity, NotifyEntity): + """Base class of PlayStation Network notify entity.""" + + group: Group | None = None + + def send_message(self, message: str, title: str | None = None) -> None: + """Send a message.""" + if TYPE_CHECKING: + assert self.group + try: + self.group.send_message(message) + except PSNAWPNotFoundError as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="group_invalid", + translation_placeholders=dict(self.translation_placeholders), + ) from e + except PSNAWPForbiddenError as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="send_message_forbidden", + translation_placeholders=dict(self.translation_placeholders), + ) from e + except (PSNAWPServerError, PSNAWPClientError) as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="send_message_failed", + translation_placeholders=dict(self.translation_placeholders), + ) from e + + +class PlaystationNetworkNotifyEntity(PlaystationNetworkNotifyBaseEntity): """Representation of a PlayStation Network notify entity.""" coordinator: PlaystationNetworkGroupsUpdateCoordinator @@ -101,26 +149,31 @@ class PlaystationNetworkNotifyEntity(PlaystationNetworkServiceEntity, NotifyEnti super().__init__(coordinator, self.entity_description) + +class PlaystationNetworkDirectMessageNotifyEntity(PlaystationNetworkNotifyBaseEntity): + """Representation of a PlayStation Network notify entity for sending direct messages.""" + + coordinator: PlaystationNetworkFriendDataCoordinator + + def __init__( + self, + coordinator: PlaystationNetworkFriendDataCoordinator, + subentry: ConfigSubentry, + ) -> None: + """Initialize a notification entity.""" + + self.entity_description = NotifyEntityDescription( + key=PlaystationNetworkNotify.DIRECT_MESSAGE, + translation_key=PlaystationNetworkNotify.DIRECT_MESSAGE, + ) + + super().__init__(coordinator, self.entity_description, subentry) + def send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" - try: - self.group.send_message(message) - except PSNAWPNotFoundError as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="group_invalid", - translation_placeholders=dict(self.translation_placeholders), - ) from e - except PSNAWPForbiddenError as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="send_message_forbidden", - translation_placeholders=dict(self.translation_placeholders), - ) from e - except (PSNAWPServerError, PSNAWPClientError) as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="send_message_failed", - translation_placeholders=dict(self.translation_placeholders), - ) from e + if not self.group: + self.group = self.coordinator.psn.psn.group( + users_list=[self.coordinator.user] + ) + super().send_message(message, title) diff --git a/homeassistant/components/playstation_network/strings.json b/homeassistant/components/playstation_network/strings.json index e5192f42873..26a1b336e2d 100644 --- a/homeassistant/components/playstation_network/strings.json +++ b/homeassistant/components/playstation_network/strings.json @@ -158,6 +158,9 @@ "notify": { "group_message": { "name": "Group: {group_name}" + }, + "direct_message": { + "name": "Direct message" } } } diff --git a/tests/components/playstation_network/snapshots/test_notify.ambr b/tests/components/playstation_network/snapshots/test_notify.ambr index 60525925787..d8c32918433 100644 --- a/tests/components/playstation_network/snapshots/test_notify.ambr +++ b/tests/components/playstation_network/snapshots/test_notify.ambr @@ -1,4 +1,53 @@ # serializer version: 1 +# name: test_notify_platform[notify.testuser_direct_message-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'notify', + 'entity_category': None, + 'entity_id': 'notify.testuser_direct_message', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Direct message', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_direct_message', + 'unit_of_measurement': None, + }) +# --- +# name: test_notify_platform[notify.testuser_direct_message-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testuser Direct message', + 'supported_features': , + }), + 'context': , + 'entity_id': 'notify.testuser_direct_message', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_notify_platform[notify.testuser_group_publicuniversalfriend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/playstation_network/test_notify.py b/tests/components/playstation_network/test_notify.py index ebaac37a09f..f81e03dfcc4 100644 --- a/tests/components/playstation_network/test_notify.py +++ b/tests/components/playstation_network/test_notify.py @@ -55,11 +55,16 @@ async def test_notify_platform( await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) +@pytest.mark.parametrize( + "entity_id", + ["notify.testuser_group_publicuniversalfriend", "notify.testuser_direct_message"], +) @freeze_time("2025-07-28T00:00:00+00:00") async def test_send_message( hass: HomeAssistant, config_entry: MockConfigEntry, mock_psnawpapi: MagicMock, + entity_id: str, ) -> None: """Test send message.""" @@ -69,7 +74,7 @@ async def test_send_message( assert config_entry.state is ConfigEntryState.LOADED - state = hass.states.get("notify.testuser_group_publicuniversalfriend") + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN @@ -77,13 +82,13 @@ async def test_send_message( NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, { - ATTR_ENTITY_ID: "notify.testuser_group_publicuniversalfriend", + ATTR_ENTITY_ID: entity_id, ATTR_MESSAGE: "henlo fren", }, blocking=True, ) - state = hass.states.get("notify.testuser_group_publicuniversalfriend") + state = hass.states.get(entity_id) assert state assert state.state == "2025-07-28T00:00:00+00:00" mock_psnawpapi.group.return_value.send_message.assert_called_once_with("henlo fren") From fa476d4e34ff2a74477215135979902162a01198 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sat, 2 Aug 2025 19:01:02 +0100 Subject: [PATCH 210/265] Fix initialisation of Apps and Radios list for Squeezebox (#149834) --- .../components/squeezebox/browse_media.py | 52 ++++++++++++++----- .../components/squeezebox/media_player.py | 5 ++ .../squeezebox/test_media_player.py | 8 +-- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index bab4f90c6d1..4f2a1fa7aa5 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -4,6 +4,7 @@ from __future__ import annotations import contextlib from dataclasses import dataclass, field +import logging from typing import Any from pysqueezebox import Player @@ -21,6 +22,8 @@ from homeassistant.helpers.network import is_internal_request from .const import DOMAIN, UNPLAYABLE_TYPES +_LOGGER = logging.getLogger(__name__) + LIBRARY = [ "favorites", "artists", @@ -138,18 +141,42 @@ class BrowseData: self.squeezebox_id_by_type.update(SQUEEZEBOX_ID_BY_TYPE) self.media_type_to_squeezebox.update(MEDIA_TYPE_TO_SQUEEZEBOX) + def add_new_command(self, cmd: str | MediaType, type: str) -> None: + """Add items to maps for new apps or radios.""" + self.known_apps_radios.add(cmd) + self.media_type_to_squeezebox[cmd] = cmd + self.squeezebox_id_by_type[cmd] = type + self.content_type_media_class[cmd] = { + "item": MediaClass.DIRECTORY, + "children": MediaClass.TRACK, + } + self.content_type_to_child_type[cmd] = MediaType.TRACK -def _add_new_command_to_browse_data( - browse_data: BrowseData, cmd: str | MediaType, type: str -) -> None: - """Add items to maps for new apps or radios.""" - browse_data.media_type_to_squeezebox[cmd] = cmd - browse_data.squeezebox_id_by_type[cmd] = type - browse_data.content_type_media_class[cmd] = { - "item": MediaClass.DIRECTORY, - "children": MediaClass.TRACK, - } - browse_data.content_type_to_child_type[cmd] = MediaType.TRACK + async def async_init(self, player: Player, browse_limit: int) -> None: + """Initialize known apps and radios from the player.""" + + cmd = ["apps", 0, browse_limit] + result = await player.async_query(*cmd) + for app in result["appss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) + cmd = ["radios", 0, browse_limit] + result = await player.async_query(*cmd) + for app in result["radioss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) def _build_response_apps_radios_category( @@ -292,8 +319,7 @@ async def build_item_response( app_cmd = "app-" + item["cmd"] if app_cmd not in browse_data.known_apps_radios: - browse_data.known_apps_radios.add(app_cmd) - _add_new_command_to_browse_data(browse_data, app_cmd, "item_id") + browse_data.add_new_command(app_cmd, "item_id") child_media = _build_response_apps_radios_category( browse_data=browse_data, cmd=app_cmd, item=item diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 0dbc1b96b0c..49aad4fd698 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -311,6 +311,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): ) return None + async def async_added_to_hass(self) -> None: + """Call when entity is added to hass.""" + await super().async_added_to_hass() + await self._browse_data.async_init(self._player, self.browse_limit) + async def async_will_remove_from_hass(self) -> None: """Remove from list of known players when removed from hass.""" self.coordinator.config_entry.runtime_data.known_player_ids.remove( diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index 440f682370b..6e3e5be0459 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -765,9 +765,7 @@ async def test_squeezebox_call_query( }, blocking=True, ) - configured_player.async_query.assert_called_once_with( - "test_command", "param1", "param2" - ) + configured_player.async_query.assert_called_with("test_command", "param1", "param2") async def test_squeezebox_call_method( @@ -784,9 +782,7 @@ async def test_squeezebox_call_method( }, blocking=True, ) - configured_player.async_query.assert_called_once_with( - "test_command", "param1", "param2" - ) + configured_player.async_query.assert_called_with("test_command", "param1", "param2") async def test_squeezebox_invalid_state( From 755864f9f3b0d0465f283b82393db7d14b38b397 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Sat, 2 Aug 2025 20:01:58 +0200 Subject: [PATCH 211/265] Add sensor platform to Qbus integration (#149389) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/qbus/climate.py | 6 +- homeassistant/components/qbus/const.py | 1 + homeassistant/components/qbus/cover.py | 6 +- homeassistant/components/qbus/entity.py | 56 +- homeassistant/components/qbus/light.py | 6 +- homeassistant/components/qbus/manifest.json | 1 + homeassistant/components/qbus/scene.py | 13 +- homeassistant/components/qbus/sensor.py | 378 ++++++ homeassistant/components/qbus/strings.json | 16 + homeassistant/components/qbus/switch.py | 6 +- tests/components/qbus/conftest.py | 21 +- .../qbus/fixtures/payload_config.json | 338 +++++- .../qbus/snapshots/test_sensor.ambr | 1047 +++++++++++++++++ tests/components/qbus/test_sensor.py | 27 + 14 files changed, 1882 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/qbus/sensor.py create mode 100644 tests/components/qbus/snapshots/test_sensor.ambr create mode 100644 tests/components/qbus/test_sensor.py diff --git a/homeassistant/components/qbus/climate.py b/homeassistant/components/qbus/climate.py index caaec2f95d7..a19ec4d0156 100644 --- a/homeassistant/components/qbus/climate.py +++ b/homeassistant/components/qbus/climate.py @@ -22,7 +22,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -42,13 +42,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "thermo", QbusClimate, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) diff --git a/homeassistant/components/qbus/const.py b/homeassistant/components/qbus/const.py index 73819d2a11b..133a3b8fea9 100644 --- a/homeassistant/components/qbus/const.py +++ b/homeassistant/components/qbus/const.py @@ -10,6 +10,7 @@ PLATFORMS: list[Platform] = [ Platform.COVER, Platform.LIGHT, Platform.SCENE, + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/qbus/cover.py b/homeassistant/components/qbus/cover.py index 2adb8253551..3fc1b20602a 100644 --- a/homeassistant/components/qbus/cover.py +++ b/homeassistant/components/qbus/cover.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -36,13 +36,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "shutter", QbusCover, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) diff --git a/homeassistant/components/qbus/entity.py b/homeassistant/components/qbus/entity.py index 91e4d83b548..9fb481d4515 100644 --- a/homeassistant/components/qbus/entity.py +++ b/homeassistant/components/qbus/entity.py @@ -14,7 +14,6 @@ from qbusmqttapi.state import QbusMqttState from homeassistant.components.mqtt import ReceiveMessage, client as mqtt from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN, MANUFACTURER from .coordinator import QbusControllerCoordinator @@ -24,14 +23,24 @@ _REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$") StateT = TypeVar("StateT", bound=QbusMqttState) -def add_new_outputs( +def create_new_entities( coordinator: QbusControllerCoordinator, added_outputs: list[QbusMqttOutput], filter_fn: Callable[[QbusMqttOutput], bool], entity_type: type[QbusEntity], - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Call async_add_entities for new outputs.""" +) -> list[QbusEntity]: + """Create entities for new outputs.""" + + new_outputs = determine_new_outputs(coordinator, added_outputs, filter_fn) + return [entity_type(output) for output in new_outputs] + + +def determine_new_outputs( + coordinator: QbusControllerCoordinator, + added_outputs: list[QbusMqttOutput], + filter_fn: Callable[[QbusMqttOutput], bool], +) -> list[QbusMqttOutput]: + """Determine new outputs.""" added_ref_ids = {k.ref_id for k in added_outputs} @@ -43,7 +52,8 @@ def add_new_outputs( if new_outputs: added_outputs.extend(new_outputs) - async_add_entities([entity_type(output) for output in new_outputs]) + + return new_outputs def format_ref_id(ref_id: str) -> str | None: @@ -67,7 +77,13 @@ class QbusEntity(Entity, Generic[StateT], ABC): _attr_has_entity_name = True _attr_should_poll = False - def __init__(self, mqtt_output: QbusMqttOutput) -> None: + def __init__( + self, + mqtt_output: QbusMqttOutput, + *, + id_suffix: str = "", + link_to_main_device: bool = False, + ) -> None: """Initialize the Qbus entity.""" self._mqtt_output = mqtt_output @@ -79,17 +95,25 @@ class QbusEntity(Entity, Generic[StateT], ABC): ) ref_id = format_ref_id(mqtt_output.ref_id) + unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}" - self._attr_unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}" + if id_suffix: + unique_id += f"_{id_suffix}" - # Create linked device - self._attr_device_info = DeviceInfo( - name=mqtt_output.name.title(), - manufacturer=MANUFACTURER, - identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")}, - suggested_area=mqtt_output.location.title(), - via_device=create_main_device_identifier(mqtt_output), - ) + self._attr_unique_id = unique_id + + if link_to_main_device: + self._attr_device_info = DeviceInfo( + identifiers={create_main_device_identifier(mqtt_output)} + ) + else: + self._attr_device_info = DeviceInfo( + name=mqtt_output.name.title(), + manufacturer=MANUFACTURER, + identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")}, + suggested_area=mqtt_output.location.title(), + via_device=create_main_device_identifier(mqtt_output), + ) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/homeassistant/components/qbus/light.py b/homeassistant/components/qbus/light.py index 4385cfe60f0..61225f11243 100644 --- a/homeassistant/components/qbus/light.py +++ b/homeassistant/components/qbus/light.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.color import brightness_to_value, value_to_brightness from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -27,13 +27,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "analog", QbusLight, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) diff --git a/homeassistant/components/qbus/manifest.json b/homeassistant/components/qbus/manifest.json index feffa6e492c..15392f6cc97 100644 --- a/homeassistant/components/qbus/manifest.json +++ b/homeassistant/components/qbus/manifest.json @@ -7,6 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/qbus", "integration_type": "hub", "iot_class": "local_push", + "loggers": ["qbusmqttapi"], "mqtt": [ "cloudapp/QBUSMQTTGW/state", "cloudapp/QBUSMQTTGW/config", diff --git a/homeassistant/components/qbus/scene.py b/homeassistant/components/qbus/scene.py index 8d18feb26d3..706fb089dde 100644 --- a/homeassistant/components/qbus/scene.py +++ b/homeassistant/components/qbus/scene.py @@ -7,11 +7,10 @@ from qbusmqttapi.state import QbusMqttState, StateAction, StateType from homeassistant.components.scene import Scene from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs, create_main_device_identifier +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -27,13 +26,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "scene", QbusScene, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) @@ -45,12 +44,8 @@ class QbusScene(QbusEntity, Scene): def __init__(self, mqtt_output: QbusMqttOutput) -> None: """Initialize scene entity.""" - super().__init__(mqtt_output) + super().__init__(mqtt_output, link_to_main_device=True) - # Add to main controller device - self._attr_device_info = DeviceInfo( - identifiers={create_main_device_identifier(mqtt_output)} - ) self._attr_name = mqtt_output.name.title() async def async_activate(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/qbus/sensor.py b/homeassistant/components/qbus/sensor.py new file mode 100644 index 00000000000..e983e0a8cbb --- /dev/null +++ b/homeassistant/components/qbus/sensor.py @@ -0,0 +1,378 @@ +"""Support for Qbus sensor.""" + +from dataclasses import dataclass + +from qbusmqttapi.discovery import QbusMqttOutput +from qbusmqttapi.state import ( + GaugeStateProperty, + QbusMqttGaugeState, + QbusMqttHumidityState, + QbusMqttThermoState, + QbusMqttVentilationState, + QbusMqttWeatherState, +) + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfLength, + UnitOfPower, + UnitOfPressure, + UnitOfSoundPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolume, + UnitOfVolumeFlowRate, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import QbusConfigEntry +from .entity import QbusEntity, create_new_entities, determine_new_outputs + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class QbusWeatherDescription(SensorEntityDescription): + """Description for Qbus weather entities.""" + + property: str + + +_WEATHER_DESCRIPTIONS = ( + QbusWeatherDescription( + key="daylight", + property="dayLight", + translation_key="daylight", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="light", + property="light", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="light_east", + property="lightEast", + translation_key="light_east", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="light_south", + property="lightSouth", + translation_key="light_south", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="light_west", + property="lightWest", + translation_key="light_west", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="temperature", + property="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + QbusWeatherDescription( + key="wind", + property="wind", + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + ), +) + +_GAUGE_VARIANT_DESCRIPTIONS = { + "AIRPRESSURE": SensorEntityDescription( + key="airpressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=UnitOfPressure.MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + "AIRQUALITY": SensorEntityDescription( + key="airquality", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + "CURRENT": SensorEntityDescription( + key="current", + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + ), + "ENERGY": SensorEntityDescription( + key="energy", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + ), + "GAS": SensorEntityDescription( + key="gas", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + "GASFLOW": SensorEntityDescription( + key="gasflow", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + "HUMIDITY": SensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "LIGHT": SensorEntityDescription( + key="light", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + "LOUDNESS": SensorEntityDescription( + key="loudness", + device_class=SensorDeviceClass.SOUND_PRESSURE, + native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, + state_class=SensorStateClass.MEASUREMENT, + ), + "POWER": SensorEntityDescription( + key="power", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + "PRESSURE": SensorEntityDescription( + key="pressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=UnitOfPressure.KPA, + state_class=SensorStateClass.MEASUREMENT, + ), + "TEMPERATURE": SensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + "VOLTAGE": SensorEntityDescription( + key="voltage", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + "VOLUME": SensorEntityDescription( + key="volume", + device_class=SensorDeviceClass.VOLUME_STORAGE, + native_unit_of_measurement=UnitOfVolume.LITERS, + state_class=SensorStateClass.MEASUREMENT, + ), + "WATER": SensorEntityDescription( + key="water", + device_class=SensorDeviceClass.WATER, + native_unit_of_measurement=UnitOfVolume.LITERS, + state_class=SensorStateClass.TOTAL, + ), + "WATERFLOW": SensorEntityDescription( + key="waterflow", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + "WATERLEVEL": SensorEntityDescription( + key="waterlevel", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.METERS, + state_class=SensorStateClass.MEASUREMENT, + ), + "WATERPRESSURE": SensorEntityDescription( + key="waterpressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=UnitOfPressure.MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + "WIND": SensorEntityDescription( + key="wind", + device_class=SensorDeviceClass.WIND_SPEED, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def _is_gauge_with_variant(output: QbusMqttOutput) -> bool: + return ( + output.type == "gauge" + and isinstance(output.variant, str) + and _GAUGE_VARIANT_DESCRIPTIONS.get(output.variant.upper()) is not None + ) + + +def _is_ventilation_with_co2(output: QbusMqttOutput) -> bool: + return output.type == "ventilation" and output.properties.get("co2") is not None + + +async def async_setup_entry( + hass: HomeAssistant, + entry: QbusConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up sensor entities.""" + + coordinator = entry.runtime_data + added_outputs: list[QbusMqttOutput] = [] + + def _create_weather_entities() -> list[QbusEntity]: + new_outputs = determine_new_outputs( + coordinator, added_outputs, lambda output: output.type == "weatherstation" + ) + + return [ + QbusWeatherSensor(output, description) + for output in new_outputs + for description in _WEATHER_DESCRIPTIONS + ] + + def _check_outputs() -> None: + entities: list[QbusEntity] = [ + *create_new_entities( + coordinator, + added_outputs, + _is_gauge_with_variant, + QbusGaugeVariantSensor, + ), + *create_new_entities( + coordinator, + added_outputs, + lambda output: output.type == "humidity", + QbusHumiditySensor, + ), + *create_new_entities( + coordinator, + added_outputs, + lambda output: output.type == "thermo", + QbusThermoSensor, + ), + *create_new_entities( + coordinator, + added_outputs, + _is_ventilation_with_co2, + QbusVentilationSensor, + ), + *_create_weather_entities(), + ] + + async_add_entities(entities) + + _check_outputs() + entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) + + +class QbusGaugeVariantSensor(QbusEntity, SensorEntity): + """Representation of a Qbus sensor entity for gauges with variant.""" + + _state_cls = QbusMqttGaugeState + + _attr_name = None + _attr_suggested_display_precision = 2 + + def __init__(self, mqtt_output: QbusMqttOutput) -> None: + """Initialize sensor entity.""" + + super().__init__(mqtt_output) + + variant = str(mqtt_output.variant) + self.entity_description = _GAUGE_VARIANT_DESCRIPTIONS[variant.upper()] + + async def _handle_state_received(self, state: QbusMqttGaugeState) -> None: + self._attr_native_value = state.read_value(GaugeStateProperty.CURRENT_VALUE) + + +class QbusHumiditySensor(QbusEntity, SensorEntity): + """Representation of a Qbus sensor entity for humidity modules.""" + + _state_cls = QbusMqttHumidityState + + _attr_device_class = SensorDeviceClass.HUMIDITY + _attr_name = None + _attr_native_unit_of_measurement = PERCENTAGE + _attr_state_class = SensorStateClass.MEASUREMENT + + async def _handle_state_received(self, state: QbusMqttHumidityState) -> None: + self._attr_native_value = state.read_value() + + +class QbusThermoSensor(QbusEntity, SensorEntity): + """Representation of a Qbus sensor entity for thermostats.""" + + _state_cls = QbusMqttThermoState + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + + async def _handle_state_received(self, state: QbusMqttThermoState) -> None: + self._attr_native_value = state.read_current_temperature() + + +class QbusVentilationSensor(QbusEntity, SensorEntity): + """Representation of a Qbus sensor entity for ventilations.""" + + _state_cls = QbusMqttVentilationState + + _attr_device_class = SensorDeviceClass.CO2 + _attr_name = None + _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_suggested_display_precision = 0 + + async def _handle_state_received(self, state: QbusMqttVentilationState) -> None: + self._attr_native_value = state.read_co2() + + +class QbusWeatherSensor(QbusEntity, SensorEntity): + """Representation of a Qbus weather sensor.""" + + _state_cls = QbusMqttWeatherState + + entity_description: QbusWeatherDescription + + def __init__( + self, mqtt_output: QbusMqttOutput, description: QbusWeatherDescription + ) -> None: + """Initialize sensor entity.""" + + super().__init__(mqtt_output, id_suffix=description.key) + + self.entity_description = description + + if description.key == "temperature": + self._attr_name = None + + async def _handle_state_received(self, state: QbusMqttWeatherState) -> None: + if value := state.read_property(self.entity_description.property, None): + self.native_value = value diff --git a/homeassistant/components/qbus/strings.json b/homeassistant/components/qbus/strings.json index f308c5b3519..f3a0d108476 100644 --- a/homeassistant/components/qbus/strings.json +++ b/homeassistant/components/qbus/strings.json @@ -16,6 +16,22 @@ "no_controller": "No controllers were found" } }, + "entity": { + "sensor": { + "daylight": { + "name": "Daylight" + }, + "light_east": { + "name": "Illuminance east" + }, + "light_south": { + "name": "Illuminance south" + }, + "light_west": { + "name": "Illuminance west" + } + } + }, "exceptions": { "invalid_preset": { "message": "Preset mode \"{preset}\" is not valid. Valid preset modes are: {options}." diff --git a/homeassistant/components/qbus/switch.py b/homeassistant/components/qbus/switch.py index 05283a44cfc..3c4d280fa30 100644 --- a/homeassistant/components/qbus/switch.py +++ b/homeassistant/components/qbus/switch.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -26,13 +26,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "onoff", QbusSwitch, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) diff --git a/tests/components/qbus/conftest.py b/tests/components/qbus/conftest.py index 9b42a6a3de8..e1febea524b 100644 --- a/tests/components/qbus/conftest.py +++ b/tests/components/qbus/conftest.py @@ -1,6 +1,6 @@ """Test fixtures for qbus.""" -from collections.abc import Generator +from collections.abc import Awaitable, Callable, Generator import json from unittest.mock import AsyncMock, patch @@ -64,3 +64,22 @@ async def setup_integration( async_fire_mqtt_message(hass, TOPIC_CONFIG, json.dumps(payload_config)) await hass.async_block_till_done() + + +@pytest.fixture +async def setup_integration_deferred( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + mock_config_entry: MockConfigEntry, + payload_config: JsonObjectType, +) -> Callable[[], Awaitable]: + """Set up the integration.""" + + async def run() -> None: + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, TOPIC_CONFIG, json.dumps(payload_config)) + await hass.async_block_till_done() + + return run diff --git a/tests/components/qbus/fixtures/payload_config.json b/tests/components/qbus/fixtures/payload_config.json index 2cad6c623db..883eca19276 100644 --- a/tests/components/qbus/fixtures/payload_config.json +++ b/tests/components/qbus/fixtures/payload_config.json @@ -116,7 +116,7 @@ { "id": "UL30", "location": "Guest bedroom", - "locationId": 0, + "locationId": 3, "name": "CURTAINS", "originalName": "CURTAINS", "refId": "000001/108", @@ -144,7 +144,7 @@ }, "id": "UL31", "location": "Living", - "locationId": 8, + "locationId": 0, "name": "SLATS", "originalName": "SLATS", "properties": { @@ -183,6 +183,340 @@ }, "refId": "000001/4", "type": "shutter" + }, + { + "id": "UL40", + "location": "Tuin", + "locationId": 12, + "name": "Luchtdruk", + "originalName": "Luchtdruk", + "refId": "000001/81", + "type": "gauge", + "variant": "AirPressure", + "actions": {}, + "properties": { + "currentValue": { + "max": 1500, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "mbar", + "write": false + } + } + }, + { + "id": "UL41", + "location": "Tuin", + "locationId": 12, + "name": "Luchtkwaliteit", + "originalName": "Luchtkwaliteit", + "refId": "000001/82", + "type": "gauge", + "variant": "AirQuality", + "actions": {}, + "properties": { + "currentValue": { + "max": 1500, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "ppm", + "write": false + } + } + }, + { + "id": "UL42", + "location": "Garage", + "locationId": 27, + "name": "Stroom", + "originalName": "Stroom", + "refId": "000001/83", + "type": "gauge", + "variant": "Current", + "actions": {}, + "properties": { + "currentValue": { + "max": 100, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "kWh", + "write": false + } + } + }, + { + "id": "UL43", + "location": "Garage", + "locationId": 27, + "name": "Energie", + "originalName": "Energie", + "refId": "000001/84", + "type": "gauge", + "variant": "Energy", + "actions": {}, + "properties": { + "currentValue": { + "read": true, + "step": 0.1, + "type": "number", + "unit": "A", + "write": false + } + } + }, + { + "id": "UL44", + "location": "Garage", + "locationId": 27, + "name": "Gas", + "originalName": "Gas", + "refId": "000001/85", + "type": "gauge", + "variant": "Gas", + "actions": {}, + "properties": { + "currentValue": { + "max": 5, + "min": 0, + "read": true, + "step": 0.001, + "type": "number", + "unit": "m³/h", + "write": false + } + } + }, + { + "id": "UL45", + "location": "Garage", + "locationId": 27, + "name": "Gas flow", + "originalName": "Gas flow", + "refId": "000001/86", + "type": "gauge", + "variant": "GasFlow", + "actions": {}, + "properties": { + "currentValue": { + "max": 10, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "m³/h", + "write": false + } + } + }, + { + "id": "UL46", + "location": "Living", + "locationId": 0, + "name": "Vochtigheid living", + "originalName": "Vochtigheid living", + "refId": "000001/87", + "type": "gauge", + "variant": "Humidity", + "actions": {}, + "properties": { + "currentValue": { + "max": 100, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "%", + "write": false + } + } + }, + { + "id": "UL47", + "location": "Tuin", + "locationId": 12, + "name": "Lichtsterkte tuin", + "originalName": "Lichtsterkte tuin", + "refId": "000001/88", + "type": "gauge", + "variant": "Light", + "actions": {}, + "properties": { + "currentValue": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "lx", + "write": false + } + } + }, + { + "id": "UL40", + "location": "Tuin", + "locationId": 12, + "name": "Regenput", + "originalName": "Regenput", + "refId": "000001/40", + "type": "gauge", + "variant": "WaterLevel", + "actions": {}, + "properties": { + "currentValue": { + "max": 100, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "m", + "write": false + } + } + }, + { + "id": "UL60", + "location": "Tuin", + "locationId": 12, + "name": "Weersensor", + "originalName": "Weersensor", + "refId": "000001/21007", + "type": "weatherstation", + "variant": [null], + "actions": {}, + "properties": { + "dayLight": { + "max": 1000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "light": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "lightEast": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "lightSouth": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "lightWest": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "raining": { + "read": true, + "type": "boolean", + "write": false + }, + "temperature": { + "max": 100, + "min": -100, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "twilight": { + "read": true, + "type": "boolean", + "write": false + }, + "wind": { + "max": 1000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + } + } + }, + { + "id": "UL70", + "location": "", + "locationId": 0, + "name": "Luchtsensor", + "originalName": "Luchtsensor", + "refId": "000001/224", + "type": "ventilation", + "variant": [null], + "actions": {}, + "properties": { + "co2": { + "max": 5000, + "min": 0, + "read": true, + "step": 16, + "type": "number", + "unit": "ppm", + "write": false + }, + "currRegime": { + "enumValues": ["Manueel", "Nacht", "Boost", "Uit", "Auto"], + "read": true, + "type": "enumString", + "write": true + }, + "refresh": { + "max": 100, + "min": 0, + "read": true, + "step": 1, + "type": "number", + "write": true + } + } + }, + { + "id": "UL80", + "location": "Kitchen", + "locationId": 8, + "name": "Vochtigheid keuken", + "originalName": "Vochtigheid keuken", + "properties": { + "currRegime": { + "enumValues": ["Manual", "Cook", "Boost", "Off", "Auto"], + "read": true, + "type": "enumString", + "write": true + }, + "value": { + "read": true, + "step": 1, + "type": "percent", + "write": false + } + }, + "refId": "000001/94/1", + "type": "humidity" } ] } diff --git a/tests/components/qbus/snapshots/test_sensor.ambr b/tests/components/qbus/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..fe665057b1e --- /dev/null +++ b/tests/components/qbus/snapshots/test_sensor.ambr @@ -0,0 +1,1047 @@ +# serializer version: 1 +# name: test_sensor[sensor.energie-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.energie', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_84', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.energie-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energie', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energie', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.gas-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gas', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_85', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.gas-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Gas', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gas', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.gas_flow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gas_flow', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_86', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.gas_flow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Gas Flow', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gas_flow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.lichtsterkte_tuin-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.lichtsterkte_tuin', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_88', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.lichtsterkte_tuin-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Lichtsterkte Tuin', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.lichtsterkte_tuin', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.living_th_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.living_th_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_120', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.living_th_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Living Th Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.living_th_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.luchtdruk-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.luchtdruk', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_81', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.luchtdruk-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Luchtdruk', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.luchtdruk', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.luchtkwaliteit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.luchtkwaliteit', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_82', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor[sensor.luchtkwaliteit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Luchtkwaliteit', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.luchtkwaliteit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.luchtsensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.luchtsensor', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_224', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor[sensor.luchtsensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Luchtsensor', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.luchtsensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.regenput-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.regenput', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_40', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.regenput-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Regenput', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.regenput', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.stroom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.stroom', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_83', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.stroom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Stroom', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stroom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.vochtigheid_keuken-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.vochtigheid_keuken', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_94-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.vochtigheid_keuken-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Vochtigheid Keuken', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.vochtigheid_keuken', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.vochtigheid_living-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.vochtigheid_living', + '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': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_87', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.vochtigheid_living-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Vochtigheid Living', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.vochtigheid_living', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.weersensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_21007_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.weersensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Weersensor', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.weersensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_daylight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.weersensor_daylight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daylight', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'daylight', + 'unique_id': 'ctd_000001_21007_daylight', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_daylight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Daylight', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_daylight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.weersensor_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_21007_light', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Illuminance', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_east-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.weersensor_illuminance_east', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance east', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_east', + 'unique_id': 'ctd_000001_21007_light_east', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_east-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Illuminance east', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_illuminance_east', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_south-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.weersensor_illuminance_south', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance south', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_south', + 'unique_id': 'ctd_000001_21007_light_south', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_south-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Illuminance south', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_illuminance_south', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_west-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_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.weersensor_illuminance_west', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance west', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_west', + 'unique_id': 'ctd_000001_21007_light_west', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_west-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Illuminance west', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_illuminance_west', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_wind_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weersensor_wind_speed', + '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': 'Wind speed', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_21007_wind', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.weersensor_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'wind_speed', + 'friendly_name': 'Weersensor Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.weersensor_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/qbus/test_sensor.py b/tests/components/qbus/test_sensor.py new file mode 100644 index 00000000000..255b29eb7f0 --- /dev/null +++ b/tests/components/qbus/test_sensor.py @@ -0,0 +1,27 @@ +"""Test Qbus sensors.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_sensor( + hass: HomeAssistant, + setup_integration_deferred: Callable[[], Awaitable], + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test sensor.""" + + with patch("homeassistant.components.qbus.PLATFORMS", [Platform.SENSOR]): + await setup_integration_deferred() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 72d9dbf39d893fac3704f883d6e8b99057c86dbe Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Sat, 2 Aug 2025 22:17:13 +0200 Subject: [PATCH 212/265] Add scopes in config flow auth request for Volvo integration (#149813) --- homeassistant/components/volvo/config_flow.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/volvo/config_flow.py b/homeassistant/components/volvo/config_flow.py index 05d19fd1d26..f187d751a2d 100644 --- a/homeassistant/components/volvo/config_flow.py +++ b/homeassistant/components/volvo/config_flow.py @@ -9,6 +9,7 @@ from typing import Any import voluptuous as vol from volvocarsapi.api import VolvoCarsApi from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle +from volvocarsapi.scopes import DEFAULT_SCOPES from homeassistant.config_entries import ( SOURCE_REAUTH, @@ -54,6 +55,13 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): self._vehicles: list[VolvoCarsVehicle] = [] self._config_data: dict = {} + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return super().extra_authorize_data | { + "scope": " ".join(DEFAULT_SCOPES), + } + @property def logger(self) -> logging.Logger: """Return logger.""" From 1236801b7d75ce7ec1b84c702079761760c30b95 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 2 Aug 2025 23:07:16 +0200 Subject: [PATCH 213/265] Fix Z-Wave config entry state conditions in listen task (#149841) --- homeassistant/components/zwave_js/__init__.py | 19 ++-- tests/components/zwave_js/conftest.py | 10 +-- tests/components/zwave_js/test_init.py | 86 ++++++++++++++++--- 3 files changed, 88 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 360969e83d4..52a5a1b7388 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1074,23 +1074,32 @@ async def client_listen( try: await client.listen(driver_ready) except BaseZwaveJSServerError as err: - if entry.state is not ConfigEntryState.LOADED: + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: raise LOGGER.error("Client listen failed: %s", err) except Exception as err: # We need to guard against unknown exceptions to not crash this task. LOGGER.exception("Unexpected exception: %s", err) - if entry.state is not ConfigEntryState.LOADED: + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: raise + if hass.is_stopping or entry.state is ConfigEntryState.UNLOAD_IN_PROGRESS: + return + + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: + raise HomeAssistantError("Listen task ended unexpectedly") + # The entry needs to be reloaded since a new driver state # will be acquired on reconnect. # All model instances will be replaced when the new state is acquired. - if not hass.is_stopping: - if entry.state is not ConfigEntryState.LOADED: - raise HomeAssistantError("Listen task ended unexpectedly") + if entry.state.recoverable: LOGGER.debug("Disconnected from server. Reloading integration") hass.config_entries.async_schedule_reload(entry.entry_id) + else: + LOGGER.error( + "Disconnected from server. Cannot recover entry %s", + entry.title, + ) async def async_unload_entry(hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> bool: diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 3c07869d5b7..eef92a7eb0a 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -565,12 +565,6 @@ def mock_listen_block_fixture() -> asyncio.Event: return asyncio.Event() -@pytest.fixture(name="listen_result") -def listen_result_fixture() -> asyncio.Future[None]: - """Mock a listen result.""" - return asyncio.Future() - - @pytest.fixture(name="client") def mock_client_fixture( controller_state: dict[str, Any], @@ -578,7 +572,6 @@ def mock_client_fixture( version_state: dict[str, Any], log_config_state: dict[str, Any], listen_block: asyncio.Event, - listen_result: asyncio.Future[None], ): """Mock a client.""" with patch( @@ -587,15 +580,16 @@ def mock_client_fixture( client = client_class.return_value async def connect(): + listen_block.clear() await asyncio.sleep(0) client.connected = True async def listen(driver_ready: asyncio.Event) -> None: driver_ready.set() await listen_block.wait() - await listen_result async def disconnect(): + listen_block.set() client.connected = False client.connect = AsyncMock(side_effect=connect) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index d9b3f392dd6..4decb061ad0 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -196,19 +196,24 @@ async def test_listen_done_during_setup_before_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, ) -> None: """Test listen task finishing during setup before forward entry.""" + listen_result = asyncio.Future[None]() assert hass.state is CoreState.running + async def connect(): + await asyncio.sleep(0) + client.connected = True + async def listen(driver_ready: asyncio.Event) -> None: await listen_block.wait() await listen_result async_fire_time_changed(hass, fire_all=True) + client.connect.side_effect = connect client.listen.side_effect = listen hass.set_state(core_state) listen_block.set() @@ -229,9 +234,9 @@ async def test_not_connected_during_setup_after_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], ) -> None: """Test we handle not connected client during setup after forward entry.""" + listen_result = asyncio.Future[None]() async def send_command_side_effect(*args: Any, **kwargs: Any) -> None: """Mock send command.""" @@ -277,12 +282,12 @@ async def test_listen_done_during_setup_after_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, ) -> None: """Test listen task finishing during setup after forward entry.""" + listen_result = asyncio.Future[None]() assert hass.state is CoreState.running original_send_command_side_effect = client.async_send_command.side_effect @@ -320,16 +325,14 @@ async def test_listen_done_during_setup_after_forward_entry( @pytest.mark.parametrize( - ("core_state", "final_config_entry_state", "disconnect_call_count"), + ("core_state", "disconnect_call_count"), [ ( CoreState.running, - ConfigEntryState.SETUP_RETRY, - 2, - ), # the reload will cause a disconnect call too + 1, + ), # the reload will cause a disconnect ( CoreState.stopping, - ConfigEntryState.LOADED, 0, ), # the home assistant stop event will handle the disconnect ], @@ -345,19 +348,33 @@ async def test_listen_done_during_setup_after_forward_entry( async def test_listen_done_after_setup( hass: HomeAssistant, client: MagicMock, - integration: MockConfigEntry, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, - final_config_entry_state: ConfigEntryState, disconnect_call_count: int, ) -> None: """Test listen task finishing after setup.""" - config_entry = integration - assert config_entry.state is ConfigEntryState.LOADED + listen_result = asyncio.Future[None]() + + async def listen(driver_ready: asyncio.Event) -> None: + driver_ready.set() + await listen_block.wait() + await listen_result + + client.listen.side_effect = listen + + config_entry = MockConfigEntry( + domain="zwave_js", + data={"url": "ws://test.org", "data_collection_opted_in": True}, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.state is CoreState.running + assert config_entry.state is ConfigEntryState.LOADED assert client.disconnect.call_count == 0 hass.set_state(core_state) @@ -365,10 +382,51 @@ async def test_listen_done_after_setup( getattr(listen_result, listen_future_result_method)(listen_future_result) await hass.async_block_till_done() - assert config_entry.state is final_config_entry_state + assert config_entry.state is ConfigEntryState.LOADED assert client.disconnect.call_count == disconnect_call_count +async def test_listen_ending_before_cancelling_listen( + hass: HomeAssistant, + integration: MockConfigEntry, + listen_block: asyncio.Event, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test listen ending during unloading before cancelling the listen task.""" + config_entry = integration + + # We can't easily simulate the race condition where the listen task ends + # before getting cancelled by the config entry during unloading. + # Use mock_state to provoke the correct condition. + config_entry.mock_state(hass, ConfigEntryState.UNLOAD_IN_PROGRESS, None) + listen_block.set() + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.UNLOAD_IN_PROGRESS + assert not any(record.levelno == logging.ERROR for record in caplog.records) + + +async def test_listen_ending_unrecoverable_config_entry_state( + hass: HomeAssistant, + integration: MockConfigEntry, + listen_block: asyncio.Event, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test listen ending when the config entry has an unrecoverable state.""" + config_entry = integration + + with patch.object( + hass.config_entries, "async_unload_platforms", return_value=False + ): + await hass.config_entries.async_unload(config_entry.entry_id) + + listen_block.set() + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.FAILED_UNLOAD + assert "Disconnected from server. Cannot recover entry" in caplog.text + + @pytest.mark.usefixtures("client") @pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) async def test_new_entity_on_value_added( From 08f7b708a453f9a791fb2070f3b0f827c88bb547 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 3 Aug 2025 09:25:17 +0200 Subject: [PATCH 214/265] Update pytest warnings filter (#149839) --- pyproject.toml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 71182e99560..2bcb7787601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -487,19 +487,10 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteofrance_api.model.forecast", # -- fixed, waiting for release / update - # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 - "ignore:.*invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base", - # https://github.com/DataDog/datadogpy/pull/566/files - >=0.37.0 - "ignore:pkg_resources is deprecated as an API:UserWarning:datadog.util.compat", # https://github.com/httplib2/httplib2/pull/226 - >=0.21.0 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2", - # https://github.com/vacanza/python-holidays/discussions/1800 - >1.0.0 - "ignore::DeprecationWarning:holidays", # https://github.com/ReactiveX/RxPY/pull/716 - >4.0.4 "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:reactivex.internal.constants", - # https://github.com/postlund/pyatv/issues/2645 - >0.16.0 - # https://github.com/postlund/pyatv/pull/2664 - "ignore:Protobuf gencode .* exactly one major version older than the runtime version 6.* at pyatv:UserWarning:google.protobuf.runtime_version", # https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.protocol", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", @@ -526,6 +517,9 @@ filterwarnings = [ "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", # https://pypi.org/project/foobot_async/ - v1.0.1 - 2024-08-16 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async", + # https://pypi.org/project/motionblindsble/ - v0.1.3 - 2024-11-12 + # https://github.com/LennP/motionblindsble/blob/0.1.3/motionblindsble/device.py#L390 + "ignore:Passing additional arguments for BLEDevice is deprecated and has no effect:DeprecationWarning:motionblindsble.device", # https://pypi.org/project/pyeconet/ - v0.1.28 - 2025-02-15 # https://github.com/w1ll1am23/pyeconet/blob/v0.1.28/src/pyeconet/api.py#L38 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api", @@ -542,8 +536,6 @@ filterwarnings = [ "ignore:Callback API version 1 is deprecated, update to latest version:DeprecationWarning:roborock.cloud_api", # https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10 "ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const", - # New in aiohttp - v3.9.0 - "ignore:It is recommended to use web.AppKey instances for keys:UserWarning:(homeassistant|tests|aiohttp_cors)", # - SyntaxWarnings # https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10 "ignore:.*invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", @@ -589,7 +581,7 @@ filterwarnings = [ # -- Websockets 14.1 # https://websockets.readthedocs.io/en/stable/howto/upgrade.html "ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy", - # https://github.com/graphql-python/gql/pull/543 - >=4.0.0a0 + # https://github.com/graphql-python/gql/pull/543 - >=4.0.0b0 "ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base", # -- unmaintained projects, last release about 2+ years From b2349ac2bdd0404ee73d7f4c918e3bff6b9d40e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sun, 3 Aug 2025 11:19:08 +0200 Subject: [PATCH 215/265] Improve miele climate test coverage (#149859) --- .../components/miele/fixtures/5_devices.json | 124 +++++++++++ .../miele/fixtures/action_fridge_freezer.json | 31 +++ .../miele/fixtures/fridge_freezer.json | 9 +- .../miele/snapshots/test_climate.ambr | 208 +++++++++++++++++- tests/components/miele/test_climate.py | 31 ++- 5 files changed, 383 insertions(+), 20 deletions(-) create mode 100644 tests/components/miele/fixtures/action_fridge_freezer.json diff --git a/tests/components/miele/fixtures/5_devices.json b/tests/components/miele/fixtures/5_devices.json index 113babbd3f7..2e76c1f6ef5 100644 --- a/tests/components/miele/fixtures/5_devices.json +++ b/tests/components/miele/fixtures/5_devices.json @@ -648,5 +648,129 @@ }, "batteryLevel": null } + }, + "DummyAppliance_12": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 12, + "value_localized": "Oven" + }, + "deviceName": "", + "protocolVersion": 4, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "16", + "techType": "H7660BP", + "matNumber": "11120960", + "swids": [] + }, + "xkmIdentLabel": { + "techType": "EK057", + "releaseVersion": "08.32" + } + }, + "state": { + "ProgramID": { + "value_raw": 356, + "value_localized": "Defrost", + "key_localized": "Program name" + }, + "status": { + "value_raw": 5, + "value_localized": "In use", + "key_localized": "status" + }, + "programType": { + "value_raw": 1, + "value_localized": "Program", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 3073, + "value_localized": "Heating-up phase", + "key_localized": "Program phase" + }, + "remainingTime": [0, 5], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": 2500, + "value_localized": 25.0, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": 1954, + "value_localized": 19.54, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": 2200, + "value_localized": 22.0, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": true + }, + "ambientLight": null, + "light": 1, + "elapsedTime": [0, 0], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } } } diff --git a/tests/components/miele/fixtures/action_fridge_freezer.json b/tests/components/miele/fixtures/action_fridge_freezer.json new file mode 100644 index 00000000000..94ee43a90fe --- /dev/null +++ b/tests/components/miele/fixtures/action_fridge_freezer.json @@ -0,0 +1,31 @@ +{ + "processAction": [6], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [], + "targetTemperature": [ + { + "zone": 1, + "min": 1, + "max": 9 + }, + { + "zone": 2, + "min": -28, + "max": -14 + }, + { + "zone": 3, + "min": -30, + "max": -15 + } + ], + "deviceName": true, + "powerOn": false, + "powerOff": false, + "colors": [], + "modes": [1], + "runOnTime": [] +} diff --git a/tests/components/miele/fixtures/fridge_freezer.json b/tests/components/miele/fixtures/fridge_freezer.json index 5d091b9c74e..8ca28befc35 100644 --- a/tests/components/miele/fixtures/fridge_freezer.json +++ b/tests/components/miele/fixtures/fridge_freezer.json @@ -53,6 +53,11 @@ "value_raw": -1800, "value_localized": -18.0, "unit": "Celsius" + }, + { + "value_raw": -2500, + "value_localized": -25.0, + "unit": "Celsius" } ], "coreTargetTemperature": [], @@ -68,8 +73,8 @@ "unit": "Celsius" }, { - "value_raw": -32768, - "value_localized": null, + "value_raw": -2800, + "value_localized": -28.0, "unit": "Celsius" } ], diff --git a/tests/components/miele/snapshots/test_climate.ambr b/tests/components/miele/snapshots/test_climate.ambr index 0fb24c893c4..3b8b7488d9b 100644 --- a/tests/components/miele/snapshots/test_climate.ambr +++ b/tests/components/miele/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_climate_states[platforms0-freezer][climate.freezer-entry] +# name: test_climate_states[freezer-platforms0][climate.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -41,7 +41,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.freezer-state] +# name: test_climate_states[freezer-platforms0][climate.freezer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -18, @@ -63,7 +63,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator-entry] +# name: test_climate_states[freezer-platforms0][climate.refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -105,7 +105,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator-state] +# name: test_climate_states[freezer-platforms0][climate.refrigerator-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 4, @@ -127,7 +127,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.freezer-entry] +# name: test_climate_states_api_push[freezer-platforms0][climate.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -169,7 +169,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.freezer-state] +# name: test_climate_states_api_push[freezer-platforms0][climate.freezer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -18, @@ -191,7 +191,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.refrigerator-entry] +# name: test_climate_states_api_push[freezer-platforms0][climate.refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -233,7 +233,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.refrigerator-state] +# name: test_climate_states_api_push[freezer-platforms0][climate.refrigerator-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 4, @@ -255,3 +255,195 @@ 'state': 'cool', }) # --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -14, + 'min_temp': -28, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_freezer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Freezer', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'freezer', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat2-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -18, + 'friendly_name': 'Fridge freezer Freezer', + 'hvac_modes': list([ + , + ]), + 'max_temp': -14, + 'min_temp': -28, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -18, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_freezer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_refrigerator', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Refrigerator', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'refrigerator', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 4, + 'friendly_name': 'Fridge freezer Refrigerator', + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': 4, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_refrigerator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -15, + 'min_temp': -30, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_zone_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Zone 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'zone_3', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat3-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -28, + 'friendly_name': 'Fridge freezer Zone 3', + 'hvac_modes': list([ + , + ]), + 'max_temp': -15, + 'min_temp': -30, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -25, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_zone_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index c4966430a9d..392a6712707 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -15,21 +15,13 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform TEST_PLATFORM = CLIMATE_DOMAIN -pytestmark = [ - pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]), - pytest.mark.parametrize( - "load_action_file", - ["action_freezer.json"], - ids=[ - "freezer", - ], - ), -] +pytestmark = pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]) ENTITY_ID = "climate.freezer" SERVICE_SET_TEMPERATURE = "set_temperature" +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_climate_states( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -42,7 +34,24 @@ async def test_climate_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.parametrize("load_device_file", ["fridge_freezer.json"]) +@pytest.mark.parametrize( + "load_action_file", ["action_fridge_freezer.json"], ids=["fridge_freezer"] +) +async def test_climate_states_mulizone( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, +) -> None: + """Test climate entity state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_climate_states_api_push( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -56,6 +65,7 @@ async def test_climate_states_api_push( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_set_target( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -74,6 +84,7 @@ async def test_set_target( ) +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_api_failure( hass: HomeAssistant, mock_miele_client: MagicMock, From fea5c63bbae4e8ddea72032829f3b678a9163ca2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 3 Aug 2025 11:23:01 +0200 Subject: [PATCH 216/265] Fix Z-Wave handling of driver ready event (#149879) --- homeassistant/components/zwave_js/__init__.py | 12 +- homeassistant/components/zwave_js/api.py | 39 +-- .../components/zwave_js/config_flow.py | 26 +- homeassistant/components/zwave_js/const.py | 4 - homeassistant/components/zwave_js/helpers.py | 55 +++- tests/components/zwave_js/test_api.py | 284 ++++++++---------- tests/components/zwave_js/test_config_flow.py | 8 +- tests/components/zwave_js/test_init.py | 35 +++ 8 files changed, 259 insertions(+), 204 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 52a5a1b7388..923cd776f92 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -105,7 +105,6 @@ from .const import ( CONF_USB_PATH, CONF_USE_ADDON, DOMAIN, - DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_VALUE_UPDATED, LIB_LOGGER, @@ -136,6 +135,7 @@ from .models import ZwaveJSConfigEntry, ZwaveJSData from .services import async_setup_services CONNECT_TIMEOUT = 10 +DRIVER_READY_TIMEOUT = 60 CONFIG_SCHEMA = vol.Schema( { @@ -368,6 +368,16 @@ class DriverEvents: ) ) + # listen for driver ready event to reload the config entry + self.config_entry.async_on_unload( + driver.on( + "driver ready", + lambda _: self.hass.config_entries.async_schedule_reload( + self.config_entry.entry_id + ), + ) + ) + # listen for new nodes being added to the mesh self.config_entry.async_on_unload( controller.on( diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 0f75d8b4673..b392b1c95cd 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections.abc import Callable, Coroutine from contextlib import suppress import dataclasses @@ -87,7 +86,6 @@ from .const import ( CONF_DATA_COLLECTION_OPTED_IN, CONF_INSTALLER_MODE, DOMAIN, - DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, USER_AGENT, @@ -98,6 +96,7 @@ from .helpers import ( async_get_node_from_device_id, async_get_provisioning_entry_from_device_id, async_get_version_info, + async_wait_for_driver_ready_event, get_device_id, ) @@ -2854,26 +2853,18 @@ async def websocket_hard_reset_controller( connection.send_result(msg[ID], device.id) async_cleanup() - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - - wait_driver_ready = asyncio.Event() - msg[DATA_UNSUBSCRIBE] = unsubs = [ async_dispatcher_connect( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added ), - driver.once("driver ready", set_driver_ready), ] + wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver) + await driver.async_hard_reset() with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() - + await wait_for_driver_ready() # When resetting the controller, the controller home id is also changed. # The controller state in the client is stale after resetting the controller, # so get the new home id with a new client using the helper function. @@ -2886,14 +2877,14 @@ async def websocket_hard_reset_controller( # The stale unique id needs to be handled by a repair flow, # after the config entry has been reloaded. LOGGER.error( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller reset" ) else: hass.config_entries.async_update_entry( entry, unique_id=str(version_info.home_id) ) - await hass.config_entries.async_reload(entry.entry_id) + hass.config_entries.async_schedule_reload(entry.entry_id) @websocket_api.websocket_command( @@ -3100,27 +3091,19 @@ async def websocket_restore_nvm( ) ) - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - - wait_driver_ready = asyncio.Event() - # Set up subscription for progress events connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), - driver.once("driver ready", set_driver_ready), ] + wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver) + await controller.async_restore_nvm_base64(msg["data"], {"preserveRoutes": False}) with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() - + await wait_for_driver_ready() # When restoring the NVM to the controller, the controller home id is also changed. # The controller state in the client is stale after restoring the NVM, # so get the new home id with a new client using the helper function. @@ -3133,14 +3116,13 @@ async def websocket_restore_nvm( # The stale unique id needs to be handled by a repair flow, # after the config entry has been reloaded. LOGGER.error( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller NVM restore" ) else: hass.config_entries.async_update_entry( entry, unique_id=str(version_info.home_id) ) - await hass.config_entries.async_reload(entry.entry_id) connection.send_message( @@ -3152,3 +3134,4 @@ async def websocket_restore_nvm( ) ) connection.send_result(msg[ID]) + async_cleanup() diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index d98dcf3dac8..308e6c9cc1a 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -62,9 +62,12 @@ from .const import ( CONF_USB_PATH, CONF_USE_ADDON, DOMAIN, - DRIVER_READY_TIMEOUT, ) -from .helpers import CannotConnect, async_get_version_info +from .helpers import ( + CannotConnect, + async_get_version_info, + async_wait_for_driver_ready_event, +) from .models import ZwaveJSConfigEntry _LOGGER = logging.getLogger(__name__) @@ -1396,19 +1399,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): event["bytesWritten"] / event["total"] * 0.5 + 0.5 ) - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - driver = self._get_driver() controller = driver.controller - wait_driver_ready = asyncio.Event() unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), - driver.once("driver ready", set_driver_ready), ] + + wait_for_driver_ready = async_wait_for_driver_ready_event(config_entry, driver) + try: await controller.async_restore_nvm( self.backup_data, {"preserveRoutes": False} @@ -1417,8 +1416,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): raise AbortFlow(f"Failed to restore network: {err}") from err else: with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() + await wait_for_driver_ready() try: version_info = await async_get_version_info( self.hass, config_entry.data[CONF_URL] @@ -1435,10 +1433,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( config_entry, unique_id=str(version_info.home_id) ) - await self.hass.config_entries.async_reload(config_entry.entry_id) - # Reload the config entry two times to clean up - # the stale device entry. + # The config entry will be also be reloaded when the driver is ready, + # by the listener in the package module, + # and two reloads are needed to clean up the stale controller device entry. # Since both the old and the new controller have the same node id, # but different hardware identifiers, the integration # will create a new device for the new controller, on the first reload, diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 6dc76ebd05d..0ccf51539d6 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -201,7 +201,3 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = { WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE, WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION, } - -# Other constants - -DRIVER_READY_TIMEOUT = 60 diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 5694be5482b..17f4909662c 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import astuple, dataclass import logging from typing import Any, cast @@ -56,6 +56,7 @@ from .const import ( ) from .models import ZwaveJSConfigEntry +DRIVER_READY_EVENT_TIMEOUT = 60 SERVER_VERSION_TIMEOUT = 10 @@ -588,5 +589,57 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio return version_info +@callback +def async_wait_for_driver_ready_event( + config_entry: ZwaveJSConfigEntry, + driver: Driver, +) -> Callable[[], Coroutine[Any, Any, None]]: + """Wait for the driver ready event and the config entry reload. + + When the driver ready event is received + the config entry will be reloaded by the integration. + This function helps wait for that to happen + before proceeding with further actions. + + If the config entry is reloaded for another reason, + this function will not wait for it to be reloaded again. + + Raises TimeoutError if the driver ready event and reload + is not received within the specified timeout. + """ + driver_ready_event_received = asyncio.Event() + config_entry_reloaded = asyncio.Event() + unsubscribers: list[Callable[[], None]] = [] + + @callback + def driver_ready_received(event: dict) -> None: + """Receive the driver ready event.""" + driver_ready_event_received.set() + + unsubscribers.append(driver.once("driver ready", driver_ready_received)) + + @callback + def on_config_entry_state_change() -> None: + """Check config entry was loaded after driver ready event.""" + if config_entry.state is ConfigEntryState.LOADED: + config_entry_reloaded.set() + + unsubscribers.append( + config_entry.async_on_state_change(on_config_entry_state_change) + ) + + async def wait_for_events() -> None: + try: + async with asyncio.timeout(DRIVER_READY_EVENT_TIMEOUT): + await asyncio.gather( + driver_ready_event_received.wait(), config_entry_reloaded.wait() + ) + finally: + for unsubscribe in unsubscribers: + unsubscribe() + + return wait_for_events + + class CannotConnect(HomeAssistantError): """Indicate connection error.""" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 6359f4bf5e7..0b83d08072c 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1,5 +1,6 @@ """Test the Z-Wave JS Websocket API.""" +import asyncio from copy import deepcopy from http import HTTPStatus from io import BytesIO @@ -5109,17 +5110,12 @@ async def test_hard_reset_controller( ws_client = await hass_ws_client(hass) assert entry.unique_id == "3245146787" - async def async_send_command_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" + async def mock_driver_hard_reset() -> None: client.driver.emit( "driver ready", {"event": "driver ready", "source": "driver"} ) - return {} - client.async_send_command.side_effect = async_send_command_driver_ready + client.driver.async_hard_reset = AsyncMock(side_effect=mock_driver_hard_reset) await ws_client.send_json_auto_id( { @@ -5128,6 +5124,7 @@ async def test_hard_reset_controller( } ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5135,16 +5132,10 @@ async def test_hard_reset_controller( assert device is not None assert msg["result"] == device.id assert msg["success"] - - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 - ) + assert client.driver.async_hard_reset.call_count == 1 assert entry.unique_id == "1234" - client.async_send_command.reset_mock() + client.driver.async_hard_reset.reset_mock() # Test client connect error when getting the server version. @@ -5158,6 +5149,7 @@ async def test_hard_reset_controller( ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5165,33 +5157,24 @@ async def test_hard_reset_controller( assert device is not None assert msg["result"] == device.id assert msg["success"] - - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 - ) + assert client.driver.async_hard_reset.call_count == 1 assert ( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller reset" ) in caplog.text - client.async_send_command.reset_mock() + client.driver.async_hard_reset.reset_mock() + get_server_version.side_effect = None # Test sending command with driver not ready and timeout. - async def async_send_command_no_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" - return {} + async def mock_driver_hard_reset_no_driver_ready() -> None: + pass - client.async_send_command.side_effect = async_send_command_no_driver_ready + client.driver.async_hard_reset.side_effect = mock_driver_hard_reset_no_driver_ready with patch( - "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT", new=0, ): await ws_client.send_json_auto_id( @@ -5201,6 +5184,7 @@ async def test_hard_reset_controller( } ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5208,32 +5192,29 @@ async def test_hard_reset_controller( assert device is not None assert msg["result"] == device.id assert msg["success"] + assert client.driver.async_hard_reset.call_count == 1 - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 - ) - - client.async_send_command.reset_mock() + client.driver.async_hard_reset.reset_mock() # Test FailedZWaveCommand is caught - with patch( - "zwave_js_server.model.driver.Driver.async_hard_reset", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - await ws_client.send_json_auto_id( - { - TYPE: "zwave_js/hard_reset_controller", - ENTRY_ID: entry.entry_id, - } - ) - msg = await ws_client.receive_json() + client.driver.async_hard_reset.side_effect = FailedZWaveCommand( + "failed_command", 1, "error message" + ) - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" - assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message" + await ws_client.send_json_auto_id( + { + TYPE: "zwave_js/hard_reset_controller", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message" + assert client.driver.async_hard_reset.call_count == 1 + + client.driver.async_hard_reset.side_effect = None # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) @@ -5578,17 +5559,24 @@ async def test_restore_nvm( # Set up mocks for the controller events controller = client.driver.controller - async def async_send_command_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" + async def mock_restore_nvm_base64( + self, base64_data: str, options: dict[str, bool] | None = None + ) -> None: + controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 150, "total": 200}, + ) + controller.data["homeId"] = 3245146787 client.driver.emit( "driver ready", {"event": "driver ready", "source": "driver"} ) - return {} - client.async_send_command.side_effect = async_send_command_driver_ready + controller.async_restore_nvm_base64 = AsyncMock(side_effect=mock_restore_nvm_base64) # Send the subscription request await ws_client.send_json_auto_id( @@ -5599,7 +5587,19 @@ async def test_restore_nvm( } ) - # Verify the finished event first + # Verify the convert progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm convert progress" + assert msg["event"]["bytesRead"] == 100 + assert msg["event"]["total"] == 200 + + # Verify the restore progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 150 + assert msg["event"]["total"] == 200 + + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" assert msg["event"]["event"] == "finished" @@ -5609,53 +5609,18 @@ async def test_restore_nvm( assert msg["type"] == "result" assert msg["success"] is True - # Simulate progress events - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 25, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 25 - assert msg["event"]["total"] == 100 - - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 50, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 50 - assert msg["event"]["total"] == 100 - await hass.async_block_till_done() # Verify the restore was called # The first call is the relevant one for nvm restore. - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) assert entry.unique_id == "1234" - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() # Test client connect error when getting the server version. @@ -5670,7 +5635,19 @@ async def test_restore_nvm( } ) - # Verify the finished event first + # Verify the convert progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm convert progress" + assert msg["event"]["bytesRead"] == 100 + assert msg["event"]["total"] == 200 + + # Verify the restore progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 150 + assert msg["event"]["total"] == 200 + + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" assert msg["event"]["event"] == "finished" @@ -5680,47 +5657,46 @@ async def test_restore_nvm( assert msg["type"] == "result" assert msg["success"] is True - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + await hass.async_block_till_done() + + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) assert ( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller NVM restore" ) in caplog.text - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() + get_server_version.side_effect = None - # Test sending command with driver not ready and timeout. + # Test sending command without driver ready event causing timeout. - async def async_send_command_no_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" - return {} + async def mock_restore_nvm_without_driver_ready( + data: bytes, options: dict[str, bool] | None = None + ): + controller.data["homeId"] = 3245146787 - client.async_send_command.side_effect = async_send_command_no_driver_ready + controller.async_restore_nvm_base64.side_effect = ( + mock_restore_nvm_without_driver_ready + ) with patch( - "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT", new=0, ): # Send the subscription request await ws_client.send_json_auto_id( { "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, + "entry_id": entry.entry_id, "data": "dGVzdA==", # base64 encoded "test" } ) - # Verify the finished event first + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" @@ -5734,37 +5710,41 @@ async def test_restore_nvm( await hass.async_block_till_done() # Verify the restore was called - # The first call is the relevant one for nvm restore. - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() # Test restore failure - with patch( - f"{CONTROLLER_PATCH_PREFIX}.async_restore_nvm_base64", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - # Send the subscription request - await ws_client.send_json_auto_id( - { - "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, - "data": "dGVzdA==", # base64 encoded "test" - } - ) + controller.async_restore_nvm_base64.side_effect = FailedZWaveCommand( + "failed_command", 1, "error message" + ) - # Verify error response - msg = await ws_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" + # Send the subscription request + await ws_client.send_json_auto_id( + { + "type": "zwave_js/restore_nvm", + "entry_id": entry.entry_id, + "data": "dGVzdA==", # base64 encoded "test" + } + ) + + # Verify error response + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + + await hass.async_block_till_done() + + # Verify the restore was called + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, + ) # Test entry_id not found await ws_client.send_json_auto_id( @@ -5779,13 +5759,13 @@ async def test_restore_nvm( assert msg["error"]["code"] == "not_found" # Test config entry not loaded - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json_auto_id( { "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, + "entry_id": entry.entry_id, "data": "dGVzdA==", # base64 encoded "test" } ) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 15ec6959caf..52b840fb690 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1101,7 +1101,7 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") with patch( - ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + ("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"), new=0, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -1111,7 +1111,7 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 4 + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3897,7 +3897,7 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") with patch( - ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + ("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"), new=0, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -3907,7 +3907,7 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 4 + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 4decb061ad0..3c39868ff93 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -2262,3 +2262,38 @@ async def test_entity_available_when_node_dead( state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) assert state assert state.state != STATE_UNAVAILABLE + + +async def test_driver_ready_event( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test receiving a driver ready event.""" + config_entry = integration + assert config_entry.state is ConfigEntryState.LOADED + + config_entry_state_changes: list[ConfigEntryState] = [] + + def on_config_entry_state_change() -> None: + """Collect config entry state changes.""" + config_entry_state_changes.append(config_entry.state) + + config_entry.async_on_state_change(on_config_entry_state_change) + + driver_ready = Event( + type="driver ready", + data={ + "source": "driver", + "event": "driver ready", + }, + ) + + client.driver.receive_event(driver_ready) + await hass.async_block_till_done() + + assert len(config_entry_state_changes) == 4 + assert config_entry_state_changes[0] == ConfigEntryState.UNLOAD_IN_PROGRESS + assert config_entry_state_changes[1] == ConfigEntryState.NOT_LOADED + assert config_entry_state_changes[2] == ConfigEntryState.SETUP_IN_PROGRESS + assert config_entry_state_changes[3] == ConfigEntryState.LOADED From 4318e29ce8732acdc881b2df9ac6f23074d0bade Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Sun, 3 Aug 2025 13:18:13 +0100 Subject: [PATCH 217/265] Bump aiomealie to 0.10.1 (#149890) --- homeassistant/components/mealie/manifest.json | 2 +- homeassistant/components/mealie/todo.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/mealie/fixtures/get_recipe.json | 4 ---- .../mealie/fixtures/get_shopping_items.json | 2 -- .../mealie/snapshots/test_diagnostics.ambr | 12 ++++++------ tests/components/mealie/snapshots/test_services.ambr | 8 ++++---- tests/components/mealie/test_todo.py | 2 -- 9 files changed, 16 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index 804011b3d9a..a744b9e6ced 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["aiomealie==0.10.0"] + "requirements": ["aiomealie==0.10.1"] } diff --git a/homeassistant/components/mealie/todo.py b/homeassistant/components/mealie/todo.py index d42c9033922..e31af281783 100644 --- a/homeassistant/components/mealie/todo.py +++ b/homeassistant/components/mealie/todo.py @@ -174,7 +174,8 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity): if list_item.display.strip() != stripped_item_summary: update_shopping_item.note = stripped_item_summary update_shopping_item.position = position - update_shopping_item.is_food = False + if update_shopping_item.is_food is not None: + update_shopping_item.is_food = False update_shopping_item.food_id = None update_shopping_item.quantity = 0.0 update_shopping_item.checked = item.status == TodoItemStatus.COMPLETED @@ -249,7 +250,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity): mutate_shopping_item.note = item.note mutate_shopping_item.checked = item.checked - if item.is_food: + if item.is_food or item.food_id: mutate_shopping_item.food_id = item.food_id mutate_shopping_item.unit_id = item.unit_id diff --git a/requirements_all.txt b/requirements_all.txt index eede7860c51..6ee3fcaa79f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -310,7 +310,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.10.0 +aiomealie==0.10.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ef659032dd..320a58eea5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -292,7 +292,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.10.0 +aiomealie==0.10.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/tests/components/mealie/fixtures/get_recipe.json b/tests/components/mealie/fixtures/get_recipe.json index a5ccd1876e5..7e42986ebdc 100644 --- a/tests/components/mealie/fixtures/get_recipe.json +++ b/tests/components/mealie/fixtures/get_recipe.json @@ -63,8 +63,6 @@ "unit": null, "food": null, "note": "130g dark couverture chocolate (min. 55% cocoa content)", - "isFood": true, - "disableAmount": false, "display": "1 130g dark couverture chocolate (min. 55% cocoa content)", "title": null, "originalText": null, @@ -87,8 +85,6 @@ "unit": null, "food": null, "note": "150g softened butter", - "isFood": true, - "disableAmount": false, "display": "1 150g softened butter", "title": null, "originalText": null, diff --git a/tests/components/mealie/fixtures/get_shopping_items.json b/tests/components/mealie/fixtures/get_shopping_items.json index 1016440816b..81db48f2e1a 100644 --- a/tests/components/mealie/fixtures/get_shopping_items.json +++ b/tests/components/mealie/fixtures/get_shopping_items.json @@ -9,8 +9,6 @@ "unit": null, "food": null, "note": "Apples", - "isFood": false, - "disableAmount": true, "display": "2 Apples", "shoppingListId": "9ce096fe-ded2-4077-877d-78ba450ab13e", "checked": false, diff --git a/tests/components/mealie/snapshots/test_diagnostics.ambr b/tests/components/mealie/snapshots/test_diagnostics.ambr index a694c72fcf6..c4d649fcec6 100644 --- a/tests/components/mealie/snapshots/test_diagnostics.ambr +++ b/tests/components/mealie/snapshots/test_diagnostics.ambr @@ -383,10 +383,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', @@ -433,10 +433,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', @@ -483,10 +483,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 257d685d8dc..a1cb758098e 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -1247,7 +1247,7 @@ 'image': 'SuPW', 'ingredients': list([ dict({ - 'is_food': True, + 'is_food': None, 'note': '130g dark couverture chocolate (min. 55% cocoa content)', 'quantity': 1.0, 'reference_id': 'a3adfe78-d157-44d8-98be-9c133e45bb4e', @@ -1261,7 +1261,7 @@ 'unit': None, }), dict({ - 'is_food': True, + 'is_food': None, 'note': '150g softened butter', 'quantity': 1.0, 'reference_id': 'f6ce06bf-8b02-43e6-8316-0dc3fb0da0fc', @@ -1763,7 +1763,7 @@ 'image': 'SuPW', 'ingredients': list([ dict({ - 'is_food': True, + 'is_food': None, 'note': '130g dark couverture chocolate (min. 55% cocoa content)', 'quantity': 1.0, 'reference_id': 'a3adfe78-d157-44d8-98be-9c133e45bb4e', @@ -1777,7 +1777,7 @@ 'unit': None, }), dict({ - 'is_food': True, + 'is_food': None, 'note': '150g softened butter', 'quantity': 1.0, 'reference_id': 'f6ce06bf-8b02-43e6-8316-0dc3fb0da0fc', diff --git a/tests/components/mealie/test_todo.py b/tests/components/mealie/test_todo.py index d156ef3a0f1..0f001cacacd 100644 --- a/tests/components/mealie/test_todo.py +++ b/tests/components/mealie/test_todo.py @@ -221,8 +221,6 @@ async def test_moving_todo_item( display=None, checked=False, position=1, - is_food=False, - disable_amount=None, quantity=2.0, label_id=None, food_id=None, From 627785edc1597df3fabc9a81c2436a1dc374ed0e Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sun, 3 Aug 2025 20:05:23 +0200 Subject: [PATCH 218/265] Fix options for error sensor in Husqvarna Automower (#149901) --- .../components/husqvarna_automower/sensor.py | 7 +- .../snapshots/test_sensor.ambr | 64 +++++++++---------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 7f2921f17fa..c5af18c6387 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -71,10 +71,10 @@ ERROR_KEYS = [ "cutting_drive_motor_2_defect", "cutting_drive_motor_3_defect", "cutting_height_blocked", + "cutting_height_problem", "cutting_height_problem_curr", "cutting_height_problem_dir", "cutting_height_problem_drive", - "cutting_height_problem", "cutting_motor_problem", "cutting_stopped_slope_too_steep", "cutting_system_blocked", @@ -117,7 +117,6 @@ ERROR_KEYS = [ "no_accurate_position_from_satellites", "no_confirmed_position", "no_drive", - "no_error", "no_loop_signal", "no_power_in_charging_station", "no_response_from_charger", @@ -169,8 +168,8 @@ ERROR_KEYS = [ ] -ERROR_KEY_LIST = list( - dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES]) +ERROR_KEY_LIST = sorted( + set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"} ) INACTIVE_REASONS: list = [ diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 3aa3504cc26..6628113d8c3 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -205,10 +205,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -219,6 +219,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -255,6 +258,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -268,6 +272,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -283,6 +288,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -300,13 +307,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'config_entry_id': , @@ -372,10 +372,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -386,6 +386,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -422,6 +425,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -435,6 +439,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -450,6 +455,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -467,13 +474,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'context': , @@ -1568,10 +1568,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -1582,6 +1582,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -1618,6 +1621,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -1631,6 +1635,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -1646,6 +1651,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -1663,13 +1670,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'config_entry_id': , @@ -1735,10 +1735,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -1749,6 +1749,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -1785,6 +1788,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -1798,6 +1802,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -1813,6 +1818,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -1830,13 +1837,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'context': , From b9e16d54c4416c4c2319a69b2280608e7985eec2 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 3 Aug 2025 20:06:14 +0200 Subject: [PATCH 219/265] Add jitter sensor to Ping integration (#149899) --- homeassistant/components/ping/helpers.py | 1 + homeassistant/components/ping/sensor.py | 11 ++++ homeassistant/components/ping/strings.json | 3 + .../ping/snapshots/test_sensor.ambr | 55 +++++++++++++++++++ tests/components/ping/test_sensor.py | 1 + 5 files changed, 71 insertions(+) diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index 996faa99c5b..8000cbcddde 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -79,6 +79,7 @@ class PingDataICMPLib(PingData): "min": data.min_rtt, "max": data.max_rtt, "avg": data.avg_rtt, + "jitter": data.jitter, } diff --git a/homeassistant/components/ping/sensor.py b/homeassistant/components/ping/sensor.py index 82d88064e02..b3866c9f0e7 100644 --- a/homeassistant/components/ping/sensor.py +++ b/homeassistant/components/ping/sensor.py @@ -71,6 +71,17 @@ SENSORS: tuple[PingSensorEntityDescription, ...] = ( value_fn=lambda result: result.data.get("min"), has_fn=lambda result: "min" in result.data, ), + PingSensorEntityDescription( + key="jitter", + translation_key="jitter", + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda result: result.data.get("jitter"), + has_fn=lambda result: "jitter" in result.data, + ), ) diff --git a/homeassistant/components/ping/strings.json b/homeassistant/components/ping/strings.json index c301a1b277d..4dc2e8ec7fc 100644 --- a/homeassistant/components/ping/strings.json +++ b/homeassistant/components/ping/strings.json @@ -12,6 +12,9 @@ }, "round_trip_time_min": { "name": "Round-trip time minimum" + }, + "jitter": { + "name": "Jitter" } } }, diff --git a/tests/components/ping/snapshots/test_sensor.ambr b/tests/components/ping/snapshots/test_sensor.ambr index f09bfe61065..8cb8642f13a 100644 --- a/tests/components/ping/snapshots/test_sensor.ambr +++ b/tests/components/ping/snapshots/test_sensor.ambr @@ -1,4 +1,59 @@ # serializer version: 1 +# name: test_setup_and_update[jitter] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.10_10_10_10_jitter', + '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': 'Jitter', + 'platform': 'ping', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'jitter', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_and_update[jitter].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '10.10.10.10 Jitter', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.10_10_10_10_jitter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.5', + }) +# --- # name: test_setup_and_update[round_trip_time_average] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ping/test_sensor.py b/tests/components/ping/test_sensor.py index bdc8b7d28e4..95a31aa5c08 100644 --- a/tests/components/ping/test_sensor.py +++ b/tests/components/ping/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.helpers import entity_registry as er "round_trip_time_maximum", "round_trip_time_mean_deviation", # should be None in the snapshot "round_trip_time_minimum", + "jitter", ], ) async def test_setup_and_update( From e0190afd3c9b5fbecb02bc13c9da9686ffa93ac7 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 3 Aug 2025 20:07:01 +0200 Subject: [PATCH 220/265] Bump `imgw_pib` to version 1.5.2 (#149892) --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 62a4f41ba1f..e65ccf35fb5 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["imgw_pib==1.5.1"] + "requirements": ["imgw_pib==1.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ee3fcaa79f..8a8c0c99615 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1240,7 +1240,7 @@ ihcsdk==2.8.5 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.1 +imgw_pib==1.5.2 # homeassistant.components.incomfort incomfort-client==0.6.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 320a58eea5e..f8e8d8865dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1074,7 +1074,7 @@ igloohome-api==0.1.1 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.1 +imgw_pib==1.5.2 # homeassistant.components.incomfort incomfort-client==0.6.9 From 084e06ec7d9dc0e79d222cc443aff05d642d8975 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 3 Aug 2025 21:46:40 +0200 Subject: [PATCH 221/265] Bump python-open-router to 0.3.1 (#149873) --- homeassistant/components/open_router/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/open_router/manifest.json b/homeassistant/components/open_router/manifest.json index fab62e7971c..8f989e63189 100644 --- a/homeassistant/components/open_router/manifest.json +++ b/homeassistant/components/open_router/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["openai==1.93.3", "python-open-router==0.3.0"] + "requirements": ["openai==1.93.3", "python-open-router==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a8c0c99615..092253636ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2481,7 +2481,7 @@ python-mpd2==3.1.1 python-mystrom==2.4.0 # homeassistant.components.open_router -python-open-router==0.3.0 +python-open-router==0.3.1 # homeassistant.components.swiss_public_transport python-opendata-transport==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8e8d8865dd..998dc9e6e6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2054,7 +2054,7 @@ python-mpd2==3.1.1 python-mystrom==2.4.0 # homeassistant.components.open_router -python-open-router==0.3.0 +python-open-router==0.3.1 # homeassistant.components.swiss_public_transport python-opendata-transport==0.5.0 From b3f830773adaa49330a82f41e5ff0d6510499ee6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 15:02:30 -1000 Subject: [PATCH 222/265] Bump yalexs-ble to 3.1.2 (#149917) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 2368c848eea..e7af7d84942 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 5b45628ee64..aa68009ac72 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 7a02afbc5d7..b1fad926f1d 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==3.1.0"] + "requirements": ["yalexs-ble==3.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 092253636ad..fd606834c7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3163,7 +3163,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==3.1.0 +yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 998dc9e6e6a..fee7ee5db48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2613,7 +2613,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==3.1.0 +yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale From 179a56628d8409390172697893a6295518beb70e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:42:11 -1000 Subject: [PATCH 223/265] Bump dbus-fast to 2.44.3 (#149921) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3b1e6e70ff6..cd6aae91259 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,7 +20,7 @@ "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", - "dbus-fast==2.44.2", + "dbus-fast==2.44.3", "habluetooth==4.0.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ac91084c4f1..c03bebc32e1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 cryptography==45.0.3 -dbus-fast==2.44.2 +dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 diff --git a/requirements_all.txt b/requirements_all.txt index fd606834c7e..23379744ee8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ datadog==0.52.0 datapoint==0.12.1 # homeassistant.components.bluetooth -dbus-fast==2.44.2 +dbus-fast==2.44.3 # homeassistant.components.debugpy debugpy==1.8.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fee7ee5db48..8654e0cfb49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ datadog==0.52.0 datapoint==0.12.1 # homeassistant.components.bluetooth -dbus-fast==2.44.2 +dbus-fast==2.44.3 # homeassistant.components.debugpy debugpy==1.8.14 From 6a8d752e56f4837b8e5086ed4b4033372c1e3bb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:42:38 -1000 Subject: [PATCH 224/265] Bump aiodiscover to 2.7.1 (#149920) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index ea2a4f4f820..599e5ecae5b 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -16,7 +16,7 @@ "quality_scale": "internal", "requirements": [ "aiodhcpwatcher==1.2.0", - "aiodiscover==2.7.0", + "aiodiscover==2.7.1", "cached-ipaddress==0.10.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c03bebc32e1..80354c706d6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ # Automatically generated by gen_requirements_all.py, do not edit aiodhcpwatcher==1.2.0 -aiodiscover==2.7.0 +aiodiscover==2.7.1 aiodns==3.5.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 23379744ee8..d0df5b416d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aiocomelit==0.12.3 aiodhcpwatcher==1.2.0 # homeassistant.components.dhcp -aiodiscover==2.7.0 +aiodiscover==2.7.1 # homeassistant.components.dnsip aiodns==3.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8654e0cfb49..aa48c183a3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ aiocomelit==0.12.3 aiodhcpwatcher==1.2.0 # homeassistant.components.dhcp -aiodiscover==2.7.0 +aiodiscover==2.7.1 # homeassistant.components.dnsip aiodns==3.5.0 From 5467db065bf08596815eede571a7e6cd28b8f451 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 07:59:47 +0200 Subject: [PATCH 225/265] Make Tuya complex type handling explicit (#149677) --- homeassistant/components/tuya/models.py | 16 ++++++++++- homeassistant/components/tuya/sensor.py | 38 +++++++++++++++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/models.py b/homeassistant/components/tuya/models.py index b4afca83a85..43e4c04c518 100644 --- a/homeassistant/components/tuya/models.py +++ b/homeassistant/components/tuya/models.py @@ -99,8 +99,22 @@ class EnumTypeData: return cls(dpcode, **parsed) +class ComplexTypeData: + """Complex Type Data (for JSON/RAW parsing).""" + + @classmethod + def from_json(cls, data: str) -> Self: + """Load JSON string and return a ComplexTypeData object.""" + raise NotImplementedError("from_json is not implemented for this type") + + @classmethod + def from_raw(cls, data: str) -> Self: + """Decode base64 string and return a ComplexTypeData object.""" + raise NotImplementedError("from_raw is not implemented for this type") + + @dataclass -class ElectricityTypeData: +class ElectricityTypeData(ComplexTypeData): """Electricity Type Data.""" electriccurrent: str | None = None diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 6e8da29ef53..da7a57b1be2 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -40,13 +40,14 @@ from .const import ( UnitOfMeasurement, ) from .entity import TuyaEntity -from .models import ElectricityTypeData, EnumTypeData, IntegerTypeData +from .models import ComplexTypeData, ElectricityTypeData, EnumTypeData, IntegerTypeData @dataclass(frozen=True) class TuyaSensorEntityDescription(SensorEntityDescription): """Describes Tuya sensor entity.""" + complex_type: type[ComplexTypeData] | None = None subkey: str | None = None @@ -368,6 +369,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -376,6 +378,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -384,6 +387,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -392,6 +396,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -400,6 +405,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -408,6 +414,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -416,6 +423,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -424,6 +432,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -432,6 +441,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1247,6 +1257,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { translation_key="total_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1255,6 +1266,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1263,6 +1275,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1271,6 +1284,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1279,6 +1293,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1287,6 +1302,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1295,6 +1311,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1303,6 +1320,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1311,6 +1329,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1319,6 +1338,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), ), @@ -1417,7 +1437,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): _status_range: DeviceStatusRange | None = None _type: DPType | None = None - _type_data: IntegerTypeData | EnumTypeData | None = None + _type_data: IntegerTypeData | EnumTypeData | ComplexTypeData | None = None _uom: UnitOfMeasurement | None = None def __init__( @@ -1516,15 +1536,21 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): # Get subkey value from Json string. if self._type is DPType.JSON: - if self.entity_description.subkey is None: + if ( + self.entity_description.complex_type is None + or self.entity_description.subkey is None + ): return None - values = ElectricityTypeData.from_json(value) + values = self.entity_description.complex_type.from_json(value) return getattr(values, self.entity_description.subkey) if self._type is DPType.RAW: - if self.entity_description.subkey is None: + if ( + self.entity_description.complex_type is None + or self.entity_description.subkey is None + ): return None - values = ElectricityTypeData.from_raw(value) + values = self.entity_description.complex_type.from_raw(value) return getattr(values, self.entity_description.subkey) # Valid string or enum value From 551dcaa1695dd43047d13d86abb2db72e87ed8e7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:08:03 +0200 Subject: [PATCH 226/265] Rename Tuya fixture files (#149927) --- tests/components/tuya/__init__.py | 90 +-- ...tor_zigbee_cover.json => cl_zah67ekd.json} | 0 ...curtain_switch.json => clkg_nhyj64w2.json} | 0 ...ector.json => co2bj_yrr3eiyiacm31ski.json} | 0 ...midifier.json => cs_ka2wfrdoogpvgzfi.json} | 0 ...dry_plus.json => cs_vmxuxszzjwp5smli.json} | 0 ...purifier.json => cs_zibqa9dutqyaxym2.json} | 0 ...or_eliminator.json => cwjwq_agwu93lr.json} | 0 ...pf100.json => cwwsq_wfkzyy0evslzsmoi.json} | 0 ...ntain.json => cwysj_z3rpyvznfcch99aa.json} | 0 ...metering.json => cz_2jxesipczks0kdct.json} | 0 ...ght_bulb.json => dj_mki13ie507rlry4r.json} | 0 ..._eawcpt.json => dlq_0tnvg2xaisqdadcf.json} | 0 ...pn_wifi.json => dlq_kxdr6su0c55p7bbo.json} | 0 ...t_light.json => gyd_lgekqfxdabipm3tn.json} | 0 ...rt_valve.json => kg_gbm9ata1zrzaez4a.json} | 0 ...ower_fan.json => kj_yrzylxax1qspdgpp.json} | 0 ...ower_fan.json => ks_j9fa8ahzac8uvlfl.json} | 0 ...ditioner.json => kt_5wnlzekkstwcdsvm.json} | 0 ...rm_host.json => mal_gyitctrjj1kefxp2.json} | 0 ..._sensor.json => mcs_7jIGJAymiH8OsFFb.json} | 0 ...ntrol.json => qccdz_7bvgooyjhiua1yyq.json} | 0 ...station.json => qxj_fsea1lat3vuktbt6.json} | 0 ...l_probe.json => qxj_is2indt9nlth6esa.json} | 0 ...sensor.json => rqbj_4iqe2hsfyd86kwwc.json} | 0 ...oller.json => sfkzq_o6dagifntoafakst.json} | 0 ...q_4_443.json => tdq_cq1p0nt0a4rixnex.json} | 0 ..._air_conditioner.json => wk_aqoouq7x.json} | 0 ...ermostat.json => wk_fi6dne5tu4t1nm6j.json} | 0 ...idity.json => wsdcg_g2y6z3p3ja2qhyav.json} | 0 ...switch.json => wxkg_l8yaz4um5b3pwyvf.json} | 0 ...ported.json => ydkt_jevroj5aguwdbs2e.json} | 0 ..._meter.json => zndb_ze8faryrxr0glqnn.json} | 0 .../snapshots/test_alarm_control_panel.ambr | 4 +- .../tuya/snapshots/test_binary_sensor.ambr | 314 ++++---- .../tuya/snapshots/test_climate.ambr | 12 +- .../components/tuya/snapshots/test_cover.ambr | 8 +- .../tuya/snapshots/test_diagnostics.ambr | 4 +- .../components/tuya/snapshots/test_event.ambr | 8 +- tests/components/tuya/snapshots/test_fan.ambr | 116 +-- .../tuya/snapshots/test_humidifier.ambr | 224 +++--- .../components/tuya/snapshots/test_init.ambr | 2 +- .../components/tuya/snapshots/test_light.ambr | 16 +- .../tuya/snapshots/test_number.ambr | 24 +- .../tuya/snapshots/test_select.ambr | 146 ++-- .../tuya/snapshots/test_sensor.ambr | 708 +++++++++--------- .../components/tuya/snapshots/test_siren.ambr | 4 +- .../tuya/snapshots/test_switch.ambr | 198 ++--- tests/components/tuya/test_binary_sensor.py | 4 +- tests/components/tuya/test_climate.py | 8 +- tests/components/tuya/test_cover.py | 10 +- tests/components/tuya/test_diagnostics.py | 4 +- tests/components/tuya/test_humidifier.py | 12 +- tests/components/tuya/test_init.py | 2 +- tests/components/tuya/test_light.py | 4 +- tests/components/tuya/test_number.py | 4 +- tests/components/tuya/test_select.py | 4 +- 57 files changed, 965 insertions(+), 965 deletions(-) rename tests/components/tuya/fixtures/{cl_am43_corded_motor_zigbee_cover.json => cl_zah67ekd.json} (100%) rename tests/components/tuya/fixtures/{clkg_curtain_switch.json => clkg_nhyj64w2.json} (100%) rename tests/components/tuya/fixtures/{co2bj_air_detector.json => co2bj_yrr3eiyiacm31ski.json} (100%) rename tests/components/tuya/fixtures/{cs_emma_dehumidifier.json => cs_ka2wfrdoogpvgzfi.json} (100%) rename tests/components/tuya/fixtures/{cs_smart_dry_plus.json => cs_vmxuxszzjwp5smli.json} (100%) rename tests/components/tuya/fixtures/{cs_arete_two_12l_dehumidifier_air_purifier.json => cs_zibqa9dutqyaxym2.json} (100%) rename tests/components/tuya/fixtures/{cwjwq_smart_odor_eliminator.json => cwjwq_agwu93lr.json} (100%) rename tests/components/tuya/fixtures/{cwwsq_cleverio_pf100.json => cwwsq_wfkzyy0evslzsmoi.json} (100%) rename tests/components/tuya/fixtures/{cwysj_pixi_smart_drinking_fountain.json => cwysj_z3rpyvznfcch99aa.json} (100%) rename tests/components/tuya/fixtures/{cz_dual_channel_metering.json => cz_2jxesipczks0kdct.json} (100%) rename tests/components/tuya/fixtures/{dj_smart_light_bulb.json => dj_mki13ie507rlry4r.json} (100%) rename tests/components/tuya/fixtures/{dlq_earu_electric_eawcpt.json => dlq_0tnvg2xaisqdadcf.json} (100%) rename tests/components/tuya/fixtures/{dlq_metering_3pn_wifi.json => dlq_kxdr6su0c55p7bbo.json} (100%) rename tests/components/tuya/fixtures/{gyd_night_light.json => gyd_lgekqfxdabipm3tn.json} (100%) rename tests/components/tuya/fixtures/{kg_smart_valve.json => kg_gbm9ata1zrzaez4a.json} (100%) rename tests/components/tuya/fixtures/{kj_bladeless_tower_fan.json => kj_yrzylxax1qspdgpp.json} (100%) rename tests/components/tuya/fixtures/{ks_tower_fan.json => ks_j9fa8ahzac8uvlfl.json} (100%) rename tests/components/tuya/fixtures/{kt_serenelife_slpac905wuk_air_conditioner.json => kt_5wnlzekkstwcdsvm.json} (100%) rename tests/components/tuya/fixtures/{mal_alarm_host.json => mal_gyitctrjj1kefxp2.json} (100%) rename tests/components/tuya/fixtures/{mcs_door_sensor.json => mcs_7jIGJAymiH8OsFFb.json} (100%) rename tests/components/tuya/fixtures/{qccdz_ac_charging_control.json => qccdz_7bvgooyjhiua1yyq.json} (100%) rename tests/components/tuya/fixtures/{qxj_weather_station.json => qxj_fsea1lat3vuktbt6.json} (100%) rename tests/components/tuya/fixtures/{qxj_temp_humidity_external_probe.json => qxj_is2indt9nlth6esa.json} (100%) rename tests/components/tuya/fixtures/{rqbj_gas_sensor.json => rqbj_4iqe2hsfyd86kwwc.json} (100%) rename tests/components/tuya/fixtures/{sfkzq_valve_controller.json => sfkzq_o6dagifntoafakst.json} (100%) rename tests/components/tuya/fixtures/{tdq_4_443.json => tdq_cq1p0nt0a4rixnex.json} (100%) rename tests/components/tuya/fixtures/{wk_air_conditioner.json => wk_aqoouq7x.json} (100%) rename tests/components/tuya/fixtures/{wk_wifi_smart_gas_boiler_thermostat.json => wk_fi6dne5tu4t1nm6j.json} (100%) rename tests/components/tuya/fixtures/{wsdcg_temperature_humidity.json => wsdcg_g2y6z3p3ja2qhyav.json} (100%) rename tests/components/tuya/fixtures/{wxkg_wireless_switch.json => wxkg_l8yaz4um5b3pwyvf.json} (100%) rename tests/components/tuya/fixtures/{ydkt_dolceclima_unsupported.json => ydkt_jevroj5aguwdbs2e.json} (100%) rename tests/components/tuya/fixtures/{zndb_smart_meter.json => zndb_ze8faryrxr0glqnn.json} (100%) diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index d793b87854a..040ee1fec2f 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -14,17 +14,17 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry DEVICE_MOCKS = { - "cl_am43_corded_motor_zigbee_cover": [ + "cl_zah67ekd": [ # https://github.com/home-assistant/core/issues/71242 Platform.COVER, Platform.SELECT, ], - "clkg_curtain_switch": [ + "clkg_nhyj64w2": [ # https://github.com/home-assistant/core/issues/136055 Platform.COVER, Platform.LIGHT, ], - "co2bj_air_detector": [ + "co2bj_yrr3eiyiacm31ski": [ # https://github.com/home-assistant/core/issues/133173 Platform.BINARY_SENSOR, Platform.NUMBER, @@ -32,15 +32,7 @@ DEVICE_MOCKS = { Platform.SENSOR, Platform.SIREN, ], - "cs_arete_two_12l_dehumidifier_air_purifier": [ - Platform.BINARY_SENSOR, - Platform.FAN, - Platform.HUMIDIFIER, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - ], - "cs_emma_dehumidifier": [ + "cs_ka2wfrdoogpvgzfi": [ # https://github.com/home-assistant/core/issues/119865 Platform.BINARY_SENSOR, Platform.FAN, @@ -49,102 +41,110 @@ DEVICE_MOCKS = { Platform.SENSOR, Platform.SWITCH, ], - "cs_smart_dry_plus": [ + "cs_vmxuxszzjwp5smli": [ # https://github.com/home-assistant/core/issues/119865 Platform.FAN, Platform.HUMIDIFIER, ], - "cwjwq_smart_odor_eliminator": [ + "cs_zibqa9dutqyaxym2": [ + Platform.BINARY_SENSOR, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ], + "cwjwq_agwu93lr": [ # https://github.com/orgs/home-assistant/discussions/79 Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ], - "cwwsq_cleverio_pf100": [ + "cwwsq_wfkzyy0evslzsmoi": [ # https://github.com/home-assistant/core/issues/144745 Platform.NUMBER, Platform.SENSOR, ], - "cwysj_pixi_smart_drinking_fountain": [ + "cwysj_z3rpyvznfcch99aa": [ # https://github.com/home-assistant/core/pull/146599 Platform.SENSOR, Platform.SWITCH, ], - "cz_dual_channel_metering": [ + "cz_2jxesipczks0kdct": [ # https://github.com/home-assistant/core/issues/147149 Platform.SENSOR, Platform.SWITCH, ], - "dj_smart_light_bulb": [ + "dj_mki13ie507rlry4r": [ # https://github.com/home-assistant/core/pull/126242 Platform.LIGHT ], - "dlq_earu_electric_eawcpt": [ + "dlq_0tnvg2xaisqdadcf": [ # https://github.com/home-assistant/core/issues/102769 Platform.SENSOR, Platform.SWITCH, ], - "dlq_metering_3pn_wifi": [ + "dlq_kxdr6su0c55p7bbo": [ # https://github.com/home-assistant/core/issues/143499 Platform.SENSOR, ], - "gyd_night_light": [ + "gyd_lgekqfxdabipm3tn": [ # https://github.com/home-assistant/core/issues/133173 Platform.LIGHT, ], - "kg_smart_valve": [ + "kg_gbm9ata1zrzaez4a": [ # https://github.com/home-assistant/core/issues/148347 Platform.SWITCH, ], - "kj_bladeless_tower_fan": [ + "kj_yrzylxax1qspdgpp": [ # https://github.com/orgs/home-assistant/discussions/61 Platform.FAN, Platform.SELECT, Platform.SWITCH, ], - "ks_tower_fan": [ + "ks_j9fa8ahzac8uvlfl": [ # https://github.com/orgs/home-assistant/discussions/329 Platform.FAN, Platform.LIGHT, Platform.SWITCH, ], - "kt_serenelife_slpac905wuk_air_conditioner": [ + "kt_5wnlzekkstwcdsvm": [ # https://github.com/home-assistant/core/pull/148646 Platform.CLIMATE, ], - "mal_alarm_host": [ + "mal_gyitctrjj1kefxp2": [ # Alarm Host support Platform.ALARM_CONTROL_PANEL, Platform.NUMBER, Platform.SWITCH, ], - "mcs_door_sensor": [ + "mcs_7jIGJAymiH8OsFFb": [ # https://github.com/home-assistant/core/issues/108301 Platform.BINARY_SENSOR, Platform.SENSOR, ], - "qccdz_ac_charging_control": [ + "qccdz_7bvgooyjhiua1yyq": [ # https://github.com/home-assistant/core/issues/136207 Platform.SWITCH, ], - "qxj_temp_humidity_external_probe": [ - # https://github.com/home-assistant/core/issues/136472 - Platform.SENSOR, - ], - "qxj_weather_station": [ + "qxj_fsea1lat3vuktbt6": [ # https://github.com/orgs/home-assistant/discussions/318 Platform.SENSOR, ], - "rqbj_gas_sensor": [ + "qxj_is2indt9nlth6esa": [ + # https://github.com/home-assistant/core/issues/136472 + Platform.SENSOR, + ], + "rqbj_4iqe2hsfyd86kwwc": [ # https://github.com/orgs/home-assistant/discussions/100 Platform.BINARY_SENSOR, Platform.SENSOR, ], - "sfkzq_valve_controller": [ + "sfkzq_o6dagifntoafakst": [ # https://github.com/home-assistant/core/issues/148116 Platform.SWITCH, ], - "tdq_4_443": [ + "tdq_cq1p0nt0a4rixnex": [ # https://github.com/home-assistant/core/issues/146845 Platform.SELECT, Platform.SWITCH, @@ -155,32 +155,32 @@ DEVICE_MOCKS = { Platform.SENSOR, Platform.SWITCH, ], - "wk_air_conditioner": [ + "wk_aqoouq7x": [ # https://github.com/home-assistant/core/issues/146263 Platform.CLIMATE, Platform.SWITCH, ], - "ydkt_dolceclima_unsupported": [ - # https://github.com/orgs/home-assistant/discussions/288 - # unsupported device - no platforms - ], - "wk_wifi_smart_gas_boiler_thermostat": [ + "wk_fi6dne5tu4t1nm6j": [ # https://github.com/orgs/home-assistant/discussions/243 Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ], - "wsdcg_temperature_humidity": [ + "wsdcg_g2y6z3p3ja2qhyav": [ # https://github.com/home-assistant/core/issues/102769 Platform.SENSOR, ], - "wxkg_wireless_switch": [ + "wxkg_l8yaz4um5b3pwyvf": [ # https://github.com/home-assistant/core/issues/93975 Platform.EVENT, Platform.SENSOR, ], - "zndb_smart_meter": [ + "ydkt_jevroj5aguwdbs2e": [ + # https://github.com/orgs/home-assistant/discussions/288 + # unsupported device - no platforms + ], + "zndb_ze8faryrxr0glqnn": [ # https://github.com/home-assistant/core/issues/138372 Platform.SENSOR, ], diff --git a/tests/components/tuya/fixtures/cl_am43_corded_motor_zigbee_cover.json b/tests/components/tuya/fixtures/cl_zah67ekd.json similarity index 100% rename from tests/components/tuya/fixtures/cl_am43_corded_motor_zigbee_cover.json rename to tests/components/tuya/fixtures/cl_zah67ekd.json diff --git a/tests/components/tuya/fixtures/clkg_curtain_switch.json b/tests/components/tuya/fixtures/clkg_nhyj64w2.json similarity index 100% rename from tests/components/tuya/fixtures/clkg_curtain_switch.json rename to tests/components/tuya/fixtures/clkg_nhyj64w2.json diff --git a/tests/components/tuya/fixtures/co2bj_air_detector.json b/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json similarity index 100% rename from tests/components/tuya/fixtures/co2bj_air_detector.json rename to tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json diff --git a/tests/components/tuya/fixtures/cs_emma_dehumidifier.json b/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json similarity index 100% rename from tests/components/tuya/fixtures/cs_emma_dehumidifier.json rename to tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json diff --git a/tests/components/tuya/fixtures/cs_smart_dry_plus.json b/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json similarity index 100% rename from tests/components/tuya/fixtures/cs_smart_dry_plus.json rename to tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json diff --git a/tests/components/tuya/fixtures/cs_arete_two_12l_dehumidifier_air_purifier.json b/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json similarity index 100% rename from tests/components/tuya/fixtures/cs_arete_two_12l_dehumidifier_air_purifier.json rename to tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json diff --git a/tests/components/tuya/fixtures/cwjwq_smart_odor_eliminator.json b/tests/components/tuya/fixtures/cwjwq_agwu93lr.json similarity index 100% rename from tests/components/tuya/fixtures/cwjwq_smart_odor_eliminator.json rename to tests/components/tuya/fixtures/cwjwq_agwu93lr.json diff --git a/tests/components/tuya/fixtures/cwwsq_cleverio_pf100.json b/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json similarity index 100% rename from tests/components/tuya/fixtures/cwwsq_cleverio_pf100.json rename to tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json diff --git a/tests/components/tuya/fixtures/cwysj_pixi_smart_drinking_fountain.json b/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json similarity index 100% rename from tests/components/tuya/fixtures/cwysj_pixi_smart_drinking_fountain.json rename to tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json diff --git a/tests/components/tuya/fixtures/cz_dual_channel_metering.json b/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json similarity index 100% rename from tests/components/tuya/fixtures/cz_dual_channel_metering.json rename to tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json diff --git a/tests/components/tuya/fixtures/dj_smart_light_bulb.json b/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json similarity index 100% rename from tests/components/tuya/fixtures/dj_smart_light_bulb.json rename to tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json diff --git a/tests/components/tuya/fixtures/dlq_earu_electric_eawcpt.json b/tests/components/tuya/fixtures/dlq_0tnvg2xaisqdadcf.json similarity index 100% rename from tests/components/tuya/fixtures/dlq_earu_electric_eawcpt.json rename to tests/components/tuya/fixtures/dlq_0tnvg2xaisqdadcf.json diff --git a/tests/components/tuya/fixtures/dlq_metering_3pn_wifi.json b/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json similarity index 100% rename from tests/components/tuya/fixtures/dlq_metering_3pn_wifi.json rename to tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json diff --git a/tests/components/tuya/fixtures/gyd_night_light.json b/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json similarity index 100% rename from tests/components/tuya/fixtures/gyd_night_light.json rename to tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json diff --git a/tests/components/tuya/fixtures/kg_smart_valve.json b/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json similarity index 100% rename from tests/components/tuya/fixtures/kg_smart_valve.json rename to tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json diff --git a/tests/components/tuya/fixtures/kj_bladeless_tower_fan.json b/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json similarity index 100% rename from tests/components/tuya/fixtures/kj_bladeless_tower_fan.json rename to tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json diff --git a/tests/components/tuya/fixtures/ks_tower_fan.json b/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json similarity index 100% rename from tests/components/tuya/fixtures/ks_tower_fan.json rename to tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json diff --git a/tests/components/tuya/fixtures/kt_serenelife_slpac905wuk_air_conditioner.json b/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json similarity index 100% rename from tests/components/tuya/fixtures/kt_serenelife_slpac905wuk_air_conditioner.json rename to tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json diff --git a/tests/components/tuya/fixtures/mal_alarm_host.json b/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json similarity index 100% rename from tests/components/tuya/fixtures/mal_alarm_host.json rename to tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json diff --git a/tests/components/tuya/fixtures/mcs_door_sensor.json b/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json similarity index 100% rename from tests/components/tuya/fixtures/mcs_door_sensor.json rename to tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json diff --git a/tests/components/tuya/fixtures/qccdz_ac_charging_control.json b/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json similarity index 100% rename from tests/components/tuya/fixtures/qccdz_ac_charging_control.json rename to tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json diff --git a/tests/components/tuya/fixtures/qxj_weather_station.json b/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json similarity index 100% rename from tests/components/tuya/fixtures/qxj_weather_station.json rename to tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json diff --git a/tests/components/tuya/fixtures/qxj_temp_humidity_external_probe.json b/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json similarity index 100% rename from tests/components/tuya/fixtures/qxj_temp_humidity_external_probe.json rename to tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json diff --git a/tests/components/tuya/fixtures/rqbj_gas_sensor.json b/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json similarity index 100% rename from tests/components/tuya/fixtures/rqbj_gas_sensor.json rename to tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json diff --git a/tests/components/tuya/fixtures/sfkzq_valve_controller.json b/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json similarity index 100% rename from tests/components/tuya/fixtures/sfkzq_valve_controller.json rename to tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json diff --git a/tests/components/tuya/fixtures/tdq_4_443.json b/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json similarity index 100% rename from tests/components/tuya/fixtures/tdq_4_443.json rename to tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json diff --git a/tests/components/tuya/fixtures/wk_air_conditioner.json b/tests/components/tuya/fixtures/wk_aqoouq7x.json similarity index 100% rename from tests/components/tuya/fixtures/wk_air_conditioner.json rename to tests/components/tuya/fixtures/wk_aqoouq7x.json diff --git a/tests/components/tuya/fixtures/wk_wifi_smart_gas_boiler_thermostat.json b/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json similarity index 100% rename from tests/components/tuya/fixtures/wk_wifi_smart_gas_boiler_thermostat.json rename to tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json diff --git a/tests/components/tuya/fixtures/wsdcg_temperature_humidity.json b/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json similarity index 100% rename from tests/components/tuya/fixtures/wsdcg_temperature_humidity.json rename to tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json diff --git a/tests/components/tuya/fixtures/wxkg_wireless_switch.json b/tests/components/tuya/fixtures/wxkg_l8yaz4um5b3pwyvf.json similarity index 100% rename from tests/components/tuya/fixtures/wxkg_wireless_switch.json rename to tests/components/tuya/fixtures/wxkg_l8yaz4um5b3pwyvf.json diff --git a/tests/components/tuya/fixtures/ydkt_dolceclima_unsupported.json b/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json similarity index 100% rename from tests/components/tuya/fixtures/ydkt_dolceclima_unsupported.json rename to tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json diff --git a/tests/components/tuya/fixtures/zndb_smart_meter.json b/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json similarity index 100% rename from tests/components/tuya/fixtures/zndb_smart_meter.json rename to tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json diff --git a/tests/components/tuya/snapshots/test_alarm_control_panel.ambr b/tests/components/tuya/snapshots/test_alarm_control_panel.ambr index 97076d5e467..73072dcb516 100644 --- a/tests/components/tuya/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/tuya/snapshots/test_alarm_control_panel.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[mal_alarm_host][alarm_control_panel.multifunction_alarm-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][alarm_control_panel.multifunction_alarm-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][alarm_control_panel.multifunction_alarm-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][alarm_control_panel.multifunction_alarm-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, diff --git a/tests/components/tuya/snapshots/test_binary_sensor.ambr b/tests/components/tuya/snapshots/test_binary_sensor.ambr index 267f61aabd0..727e59590a5 100644 --- a/tests/components/tuya/snapshots/test_binary_sensor.ambr +++ b/tests/components/tuya/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][binary_sensor.aqi_safety-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][binary_sensor.aqi_safety-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][binary_sensor.aqi_safety-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][binary_sensor.aqi_safety-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'safety', @@ -48,154 +48,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_defrost-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.dehumidifier_defrost', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Defrost', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'defrost', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqdefrost', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_defrost-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Defrost', - }), - 'context': , - 'entity_id': 'binary_sensor.dehumidifier_defrost', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_tank_full-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.dehumidifier_tank_full', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Tank full', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'tankfull', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqtankfull', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_tank_full-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Tank full', - }), - 'context': , - 'entity_id': 'binary_sensor.dehumidifier_tank_full', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_wet-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.dehumidifier_wet', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wet', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'wet', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqwet', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_wet-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Wet', - }), - 'context': , - 'entity_id': 'binary_sensor.dehumidifier_wet', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_defrost-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_defrost-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -230,7 +83,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_defrost-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_defrost-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -244,7 +97,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_tank_full-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_tank_full-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -279,7 +132,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_tank_full-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_tank_full-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -293,7 +146,154 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][binary_sensor.door_garage_door-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_defrost-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.dehumidifier_defrost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Defrost', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'defrost', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqdefrost', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_defrost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Dehumidifier Defrost', + }), + 'context': , + 'entity_id': 'binary_sensor.dehumidifier_defrost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_tank_full-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.dehumidifier_tank_full', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tank full', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tankfull', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqtankfull', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_tank_full-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Dehumidifier Tank full', + }), + 'context': , + 'entity_id': 'binary_sensor.dehumidifier_tank_full', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_wet-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.dehumidifier_wet', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wet', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wet', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqwet', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_wet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Dehumidifier Wet', + }), + 'context': , + 'entity_id': 'binary_sensor.dehumidifier_wet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -328,7 +328,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][binary_sensor.door_garage_door-state] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'door', @@ -342,7 +342,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][binary_sensor.gas_sensor_gas-entry] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -377,7 +377,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][binary_sensor.gas_sensor_gas-state] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'gas', diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index 6e93a1b263c..cb535cc5c07 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[kt_serenelife_slpac905wuk_air_conditioner][climate.air_conditioner-entry] +# name: test_platform_setup_and_discovery[kt_5wnlzekkstwcdsvm][climate.air_conditioner-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -46,7 +46,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kt_serenelife_slpac905wuk_air_conditioner][climate.air_conditioner-state] +# name: test_platform_setup_and_discovery[kt_5wnlzekkstwcdsvm][climate.air_conditioner-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 22.0, @@ -74,7 +74,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][climate.clima_cucina-entry] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][climate.clima_cucina-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -124,7 +124,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][climate.clima_cucina-state] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][climate.clima_cucina-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 27.0, @@ -155,7 +155,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][climate.wifi_smart_gas_boiler_thermostat-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][climate.wifi_smart_gas_boiler_thermostat-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -198,7 +198,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][climate.wifi_smart_gas_boiler_thermostat-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][climate.wifi_smart_gas_boiler_thermostat-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 24.9, diff --git a/tests/components/tuya/snapshots/test_cover.ambr b/tests/components/tuya/snapshots/test_cover.ambr index 6ae4781c7c1..aa592b25520 100644 --- a/tests/components/tuya/snapshots/test_cover.ambr +++ b/tests/components/tuya/snapshots/test_cover.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][cover.kitchen_blinds_curtain-entry] +# name: test_platform_setup_and_discovery[cl_zah67ekd][cover.kitchen_blinds_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][cover.kitchen_blinds_curtain-state] +# name: test_platform_setup_and_discovery[cl_zah67ekd][cover.kitchen_blinds_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 48, @@ -50,7 +50,7 @@ 'state': 'open', }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][cover.tapparelle_studio_curtain-entry] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][cover.tapparelle_studio_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -85,7 +85,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][cover.tapparelle_studio_curtain-state] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][cover.tapparelle_studio_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 0, diff --git a/tests/components/tuya/snapshots/test_diagnostics.ambr b/tests/components/tuya/snapshots/test_diagnostics.ambr index 5fc3796d109..93cc0cd0b6d 100644 --- a/tests/components/tuya/snapshots/test_diagnostics.ambr +++ b/tests/components/tuya/snapshots/test_diagnostics.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_device_diagnostics[rqbj_gas_sensor] +# name: test_device_diagnostics[rqbj_4iqe2hsfyd86kwwc] dict({ 'active_time': '2025-06-24T20:33:10+00:00', 'category': 'rqbj', @@ -88,7 +88,7 @@ 'update_time': '2025-06-24T20:33:10+00:00', }) # --- -# name: test_entry_diagnostics[rqbj_gas_sensor] +# name: test_entry_diagnostics[rqbj_4iqe2hsfyd86kwwc] dict({ 'devices': list([ dict({ diff --git a/tests/components/tuya/snapshots/test_event.ambr b/tests/components/tuya/snapshots/test_event.ambr index 085ebd3ec8b..ea19ff486da 100644 --- a/tests/components/tuya/snapshots/test_event.ambr +++ b/tests/components/tuya/snapshots/test_event.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_1-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_1-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', @@ -58,7 +58,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_2-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_2-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', diff --git a/tests/components/tuya/snapshots/test_fan.ambr b/tests/components/tuya/snapshots/test_fan.ambr index ff795c150c9..69eb1b467e9 100644 --- a/tests/components/tuya/snapshots/test_fan.ambr +++ b/tests/components/tuya/snapshots/test_fan.ambr @@ -1,55 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][fan.dehumidifier-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'fan', - 'entity_category': None, - 'entity_id': 'fan.dehumidifier', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'tuya.bf3fce6af592f12df3gbgq', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][fan.dehumidifier-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier', - 'supported_features': , - }), - 'context': , - 'entity_id': 'fan.dehumidifier', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][fan.dehumidifer-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][fan.dehumidifer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -87,7 +37,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][fan.dehumidifer-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][fan.dehumidifer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer', @@ -103,7 +53,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][fan.dehumidifier-entry] +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -139,7 +89,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][fan.dehumidifier-state] +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifier ', @@ -153,7 +103,57 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][fan.bree-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][fan.dehumidifier-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.dehumidifier', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.bf3fce6af592f12df3gbgq', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][fan.dehumidifier-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifier', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.dehumidifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -192,7 +192,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][fan.bree-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree', @@ -210,7 +210,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][fan.tower_fan_ca_407g_smart-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][fan.tower_fan_ca_407g_smart-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -251,7 +251,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][fan.tower_fan_ca_407g_smart-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][fan.tower_fan_ca_407g_smart-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Tower Fan CA-407G Smart', diff --git a/tests/components/tuya/snapshots/test_humidifier.ambr b/tests/components/tuya/snapshots/test_humidifier.ambr index 3389f927eb4..25bb1799dc8 100644 --- a/tests/components/tuya/snapshots/test_humidifier.ambr +++ b/tests/components/tuya/snapshots/test_humidifier.ambr @@ -1,5 +1,115 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][humidifier.dehumidifier-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][humidifier.dehumidifer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_humidity': 80, + 'min_humidity': 25, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'humidifier', + 'entity_category': None, + 'entity_id': 'humidifier.dehumidifer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.mock_device_idswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][humidifier.dehumidifer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'dehumidifier', + 'friendly_name': 'Dehumidifer', + 'max_humidity': 80, + 'min_humidity': 25, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.dehumidifer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_humidity': 100, + 'min_humidity': 0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'humidifier', + 'entity_category': None, + 'entity_id': 'humidifier.dehumidifier', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.mock_device_idswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'dehumidifier', + 'friendly_name': 'Dehumidifier ', + 'max_humidity': 100, + 'min_humidity': 0, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.dehumidifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][humidifier.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -37,7 +147,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][humidifier.dehumidifier-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][humidifier.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_humidity': 47, @@ -56,113 +166,3 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][humidifier.dehumidifer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_humidity': 80, - 'min_humidity': 25, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'humidifier', - 'entity_category': None, - 'entity_id': 'humidifier.dehumidifer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.mock_device_idswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][humidifier.dehumidifer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'dehumidifier', - 'friendly_name': 'Dehumidifer', - 'max_humidity': 80, - 'min_humidity': 25, - 'supported_features': , - }), - 'context': , - 'entity_id': 'humidifier.dehumidifer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][humidifier.dehumidifier-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_humidity': 100, - 'min_humidity': 0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'humidifier', - 'entity_category': None, - 'entity_id': 'humidifier.dehumidifier', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.mock_device_idswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][humidifier.dehumidifier-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'dehumidifier', - 'friendly_name': 'Dehumidifier ', - 'max_humidity': 100, - 'min_humidity': 0, - 'supported_features': , - }), - 'context': , - 'entity_id': 'humidifier.dehumidifier', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 127c764ea0d..83548abf0c3 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_unsupported_device[ydkt_dolceclima_unsupported] +# name: test_unsupported_device[ydkt_jevroj5aguwdbs2e] list([ DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index ec8e663f62c..06ad884cfa3 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[clkg_curtain_switch][light.tapparelle_studio_backlight-entry] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][light.tapparelle_studio_backlight-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -38,7 +38,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][light.tapparelle_studio_backlight-state] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][light.tapparelle_studio_backlight-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': , @@ -56,7 +56,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dj_smart_light_bulb][light.garage_light-entry] +# name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -96,7 +96,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_smart_light_bulb][light.garage_light-state] +# name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': 138, @@ -119,7 +119,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[gyd_night_light][light.colorful_pir_night_light-entry] +# name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -163,7 +163,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[gyd_night_light][light.colorful_pir_night_light-state] +# name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': None, @@ -192,7 +192,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][light.tower_fan_ca_407g_smart_backlight-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][light.tower_fan_ca_407g_smart_backlight-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -231,7 +231,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][light.tower_fan_ca_407g_smart_backlight-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][light.tower_fan_ca_407g_smart_backlight-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': , diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index 1c8af00baff..c6f2bb363b6 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][number.aqi_alarm_duration-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][number.aqi_alarm_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][number.aqi_alarm_duration-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][number.aqi_alarm_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -58,7 +58,7 @@ 'state': '1.0', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][number.cleverio_pf100_feed-entry] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][number.cleverio_pf100_feed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][number.cleverio_pf100_feed-state] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][number.cleverio_pf100_feed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Cleverio PF100 Feed', @@ -116,7 +116,7 @@ 'state': '1.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_alarm_delay-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -156,7 +156,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_alarm_delay-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -175,7 +175,7 @@ 'state': '20.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_arm_delay-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_arm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -215,7 +215,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_arm_delay-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_arm_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -234,7 +234,7 @@ 'state': '15.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_siren_duration-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_siren_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -274,7 +274,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_siren_duration-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_siren_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -293,7 +293,7 @@ 'state': '3.0', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -333,7 +333,7 @@ 'unit_of_measurement': '℃', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][number.wifi_smart_gas_boiler_thermostat_temperature_correction-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Temperature correction', diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 0f530184122..4bd058517be 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][select.kitchen_blinds_motor_mode-entry] +# name: test_platform_setup_and_discovery[cl_zah67ekd][select.kitchen_blinds_motor_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][select.kitchen_blinds_motor_mode-state] +# name: test_platform_setup_and_discovery[cl_zah67ekd][select.kitchen_blinds_motor_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Kitchen Blinds Motor mode', @@ -56,7 +56,7 @@ 'state': 'forward', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][select.aqi_volume-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][select.aqi_volume-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][select.aqi_volume-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][select.aqi_volume-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI Volume', @@ -117,68 +117,7 @@ 'state': 'low', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][select.dehumidifier_countdown-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'cancel', - '1h', - '2h', - '3h', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.dehumidifier_countdown', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Countdown', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'countdown', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqcountdown_set', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][select.dehumidifier_countdown-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier Countdown', - 'options': list([ - 'cancel', - '1h', - '2h', - '3h', - ]), - }), - 'context': , - 'entity_id': 'select.dehumidifier_countdown', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'cancel', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][select.dehumidifer_countdown-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][select.dehumidifer_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -220,7 +159,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][select.dehumidifer_countdown-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][select.dehumidifer_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer Countdown', @@ -239,7 +178,68 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][select.smart_odor_eliminator_pro_odor_elimination_mode-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][select.dehumidifier_countdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '1h', + '2h', + '3h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.dehumidifier_countdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Countdown', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'countdown', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqcountdown_set', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][select.dehumidifier_countdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifier Countdown', + 'options': list([ + 'cancel', + '1h', + '2h', + '3h', + ]), + }), + 'context': , + 'entity_id': 'select.dehumidifier_countdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cancel', + }) +# --- +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][select.smart_odor_eliminator_pro_odor_elimination_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -279,7 +279,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][select.smart_odor_eliminator_pro_odor_elimination_mode-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][select.smart_odor_eliminator_pro_odor_elimination_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Odor elimination mode', @@ -296,7 +296,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][select.bree_countdown-entry] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -340,7 +340,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][select.bree_countdown-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree Countdown', @@ -361,7 +361,7 @@ 'state': 'cancel', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][select.4_433_power_on_behavior-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -402,7 +402,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][select.4_433_power_on_behavior-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '4-433 Power on behavior', diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 80051a08396..882839a6665 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_battery-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -36,7 +36,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_battery-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -52,7 +52,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_formaldehyde-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_formaldehyde-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -89,7 +89,7 @@ 'unit_of_measurement': 'mg/m3', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_formaldehyde-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_formaldehyde-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI Formaldehyde', @@ -104,7 +104,7 @@ 'state': '0.002', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_humidity-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -141,7 +141,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_humidity-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -157,7 +157,7 @@ 'state': '53.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_temperature-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -197,7 +197,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_temperature-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -213,7 +213,7 @@ 'state': '26.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_volatile_organic_compounds-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_volatile_organic_compounds-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -250,7 +250,7 @@ 'unit_of_measurement': 'mg/m³', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_volatile_organic_compounds-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_volatile_organic_compounds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'volatile_organic_compounds', @@ -266,60 +266,7 @@ 'state': '0.018', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][sensor.dehumidifier_humidity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.dehumidifier_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqhumidity_indoor', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][sensor.dehumidifier_humidity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'Dehumidifier Humidity', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.dehumidifier_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '47.0', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][sensor.dehumidifer_humidity-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][sensor.dehumidifer_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -356,7 +303,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][sensor.dehumidifer_humidity-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][sensor.dehumidifer_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -372,7 +319,60 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_battery-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][sensor.dehumidifier_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dehumidifier_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqhumidity_indoor', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][sensor.dehumidifier_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Dehumidifier Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dehumidifier_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '47.0', + }) +# --- +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -409,7 +409,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_battery-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -425,7 +425,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_status-entry] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -460,7 +460,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_status-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_status-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Status', @@ -473,7 +473,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][sensor.cleverio_pf100_last_amount-entry] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][sensor.cleverio_pf100_last_amount-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -510,7 +510,7 @@ 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][sensor.cleverio_pf100_last_amount-state] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][sensor.cleverio_pf100_last_amount-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Cleverio PF100 Last amount', @@ -525,7 +525,7 @@ 'state': '2.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_filter_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_filter_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -565,7 +565,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_filter_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_filter_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -581,7 +581,7 @@ 'state': '18965.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_uv_runtime-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_uv_runtime-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -621,7 +621,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_uv_runtime-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_uv_runtime-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -637,7 +637,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_level-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -672,7 +672,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_level-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Water level', @@ -685,7 +685,7 @@ 'state': 'level_3', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_pump_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_pump_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -725,7 +725,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_pump_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_pump_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -741,7 +741,7 @@ 'state': '18965.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_usage_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_usage_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -781,7 +781,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_usage_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_usage_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -797,7 +797,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_current-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -840,7 +840,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_current-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -856,7 +856,7 @@ 'state': '0.083', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_power-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -896,7 +896,7 @@ 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_power-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -912,7 +912,7 @@ 'state': '6.4', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_voltage-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -955,7 +955,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_voltage-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -971,7 +971,7 @@ 'state': '121.7', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1014,7 +1014,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1030,7 +1030,7 @@ 'state': '2.198', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1070,7 +1070,7 @@ 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1086,7 +1086,7 @@ 'state': '495.3', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1129,7 +1129,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1145,7 +1145,7 @@ 'state': '231.4', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1185,7 +1185,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1201,7 +1201,7 @@ 'state': '0.637', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1241,7 +1241,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1257,7 +1257,7 @@ 'state': '0.108', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1297,7 +1297,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1313,7 +1313,7 @@ 'state': '221.1', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1353,7 +1353,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1369,7 +1369,7 @@ 'state': '11.203', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1409,7 +1409,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1425,7 +1425,7 @@ 'state': '2.41', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1465,7 +1465,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1481,7 +1481,7 @@ 'state': '218.7', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1521,7 +1521,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1537,7 +1537,7 @@ 'state': '0.913', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1577,7 +1577,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1593,7 +1593,7 @@ 'state': '0.092', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1633,7 +1633,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1649,7 +1649,7 @@ 'state': '220.4', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][sensor.door_garage_battery-entry] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1686,7 +1686,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][sensor.door_garage_battery-state] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -1702,220 +1702,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_battery_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.frysen_battery_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Battery state', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'battery_state', - 'unique_id': 'tuya.bff00f6abe0563b284t77pbattery_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_battery_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Frysen Battery state', - }), - 'context': , - 'entity_id': 'sensor.frysen_battery_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'high', - }) -# --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_humidity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_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.frysen_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.bff00f6abe0563b284t77phumidity_value', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_humidity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'Frysen Humidity', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.frysen_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '38.0', - }) -# --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_probe_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.frysen_probe_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Probe temperature', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'temperature_external', - 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current_external', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_probe_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Frysen Probe temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.frysen_probe_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '-13.0', - }) -# --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.frysen_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'temperature', - 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Frysen Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.frysen_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '22.2', - }) -# --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1950,7 +1737,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Battery state', @@ -1963,7 +1750,7 @@ 'state': 'high', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2000,7 +1787,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2016,7 +1803,7 @@ 'state': '52.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2053,7 +1840,7 @@ 'unit_of_measurement': 'lx', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'illuminance', @@ -2069,7 +1856,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2109,7 +1896,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2125,7 +1912,7 @@ 'state': '-40.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2165,7 +1952,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2181,7 +1968,220 @@ 'state': '24.0', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][sensor.gas_sensor_gas-entry] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.frysen_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.bff00f6abe0563b284t77pbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Frysen Battery state', + }), + 'context': , + 'entity_id': 'sensor.frysen_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frysen_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.bff00f6abe0563b284t77phumidity_value', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Frysen Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.frysen_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.0', + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_probe_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frysen_probe_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_external', + 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current_external', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_probe_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frysen Probe temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frysen_probe_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-13.0', + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frysen_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frysen Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frysen_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.2', + }) +# --- +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2218,7 +2218,7 @@ 'unit_of_measurement': 'ppm', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][sensor.gas_sensor_gas-state] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][sensor.gas_sensor_gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Gas sensor Gas', @@ -2334,7 +2334,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2371,7 +2371,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][sensor.wifi_smart_gas_boiler_thermostat_battery-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][sensor.wifi_smart_gas_boiler_thermostat_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2387,7 +2387,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_battery-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2424,7 +2424,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_battery-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2440,7 +2440,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_humidity-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2477,7 +2477,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_humidity-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2493,7 +2493,7 @@ 'state': '47.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_temperature-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2533,7 +2533,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_temperature-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2549,7 +2549,7 @@ 'state': '18.5', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][sensor.bathroom_smart_switch_battery-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][sensor.bathroom_smart_switch_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2586,7 +2586,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][sensor.bathroom_smart_switch_battery-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][sensor.bathroom_smart_switch_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2602,7 +2602,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_current-entry] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2642,7 +2642,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_current-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -2658,7 +2658,7 @@ 'state': '5.62', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_power-entry] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2698,7 +2698,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_power-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -2714,7 +2714,7 @@ 'state': '1.185', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_voltage-entry] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2754,7 +2754,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_voltage-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', diff --git a/tests/components/tuya/snapshots/test_siren.ambr b/tests/components/tuya/snapshots/test_siren.ambr index 8a6faa31c43..7b6afe9dc60 100644 --- a/tests/components/tuya/snapshots/test_siren.ambr +++ b/tests/components/tuya/snapshots/test_siren.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][siren.aqi-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][siren.aqi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][siren.aqi-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][siren.aqi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI', diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index e21fe9c91bd..2c2325e9ed8 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1,54 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][switch.dehumidifier_child_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.dehumidifier_child_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:account-lock', - 'original_name': 'Child lock', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'child_lock', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqchild_lock', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][switch.dehumidifier_child_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier Child lock', - 'icon': 'mdi:account-lock', - }), - 'context': , - 'entity_id': 'switch.dehumidifier_child_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_child_lock-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -83,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_child_lock-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer Child lock', @@ -97,7 +48,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_ionizer-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_ionizer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -132,7 +83,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_ionizer-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_ionizer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer Ionizer', @@ -146,7 +97,56 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][switch.smart_odor_eliminator_pro_switch-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][switch.dehumidifier_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.dehumidifier_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:account-lock', + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][switch.dehumidifier_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifier Child lock', + 'icon': 'mdi:account-lock', + }), + 'context': , + 'entity_id': 'switch.dehumidifier_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][switch.smart_odor_eliminator_pro_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -181,7 +181,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][switch.smart_odor_eliminator_pro_switch-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][switch.smart_odor_eliminator_pro_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Switch', @@ -194,7 +194,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_filter_reset-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_filter_reset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -229,7 +229,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_filter_reset-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_filter_reset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Filter reset', @@ -242,7 +242,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_power-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -277,7 +277,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_power-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Power', @@ -290,7 +290,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -325,7 +325,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Reset of water usage days', @@ -338,7 +338,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_uv_sterilization-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_uv_sterilization-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -373,7 +373,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_uv_sterilization-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_uv_sterilization-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain UV sterilization', @@ -386,7 +386,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_water_pump_reset-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_water_pump_reset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -421,7 +421,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_water_pump_reset-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_water_pump_reset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Water pump reset', @@ -434,7 +434,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_1-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -469,7 +469,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_1-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -483,7 +483,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_2-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -518,7 +518,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_2-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -532,7 +532,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -567,7 +567,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '一路带计量磁保持通断器 Child lock', @@ -580,7 +580,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -615,7 +615,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '一路带计量磁保持通断器 Switch', @@ -628,7 +628,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[kg_smart_valve][switch.qt_switch_switch_1-entry] +# name: test_platform_setup_and_discovery[kg_gbm9ata1zrzaez4a][switch.qt_switch_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -663,7 +663,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kg_smart_valve][switch.qt_switch_switch_1-state] +# name: test_platform_setup_and_discovery[kg_gbm9ata1zrzaez4a][switch.qt_switch_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -677,7 +677,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][switch.bree_power-entry] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -712,7 +712,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][switch.bree_power-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree Power', @@ -725,7 +725,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][switch.tower_fan_ca_407g_smart_ionizer-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][switch.tower_fan_ca_407g_smart_ionizer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -760,7 +760,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][switch.tower_fan_ca_407g_smart_ionizer-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][switch.tower_fan_ca_407g_smart_ionizer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Tower Fan CA-407G Smart Ionizer', @@ -773,7 +773,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_arm_beep-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_arm_beep-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -808,7 +808,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_arm_beep-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_arm_beep-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Multifunction alarm Arm beep', @@ -821,7 +821,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_siren-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_siren-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -856,7 +856,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_siren-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_siren-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Multifunction alarm Siren', @@ -869,7 +869,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[qccdz_ac_charging_control][switch.ac_charging_control_box_switch-entry] +# name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -904,7 +904,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qccdz_ac_charging_control][switch.ac_charging_control_box_switch-state] +# name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AC charging control box Switch', @@ -917,7 +917,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sfkzq_valve_controller][switch.sprinkler_cesare_switch-entry] +# name: test_platform_setup_and_discovery[sfkzq_o6dagifntoafakst][switch.sprinkler_cesare_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -952,7 +952,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sfkzq_valve_controller][switch.sprinkler_cesare_switch-state] +# name: test_platform_setup_and_discovery[sfkzq_o6dagifntoafakst][switch.sprinkler_cesare_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Sprinkler Cesare Switch', @@ -965,7 +965,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_1-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1000,7 +1000,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_1-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1014,7 +1014,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_2-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1049,7 +1049,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_2-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1063,7 +1063,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_3-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1098,7 +1098,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_3-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1112,7 +1112,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_4-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_4-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1147,7 +1147,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_4-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1209,7 +1209,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][switch.clima_cucina_child_lock-entry] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][switch.clima_cucina_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1244,7 +1244,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][switch.clima_cucina_child_lock-state] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][switch.clima_cucina_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Clima cucina Child lock', @@ -1257,7 +1257,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][switch.wifi_smart_gas_boiler_thermostat_child_lock-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][switch.wifi_smart_gas_boiler_thermostat_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1292,7 +1292,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][switch.wifi_smart_gas_boiler_thermostat_child_lock-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][switch.wifi_smart_gas_boiler_thermostat_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Child lock', diff --git a/tests/components/tuya/test_binary_sensor.py b/tests/components/tuya/test_binary_sensor.py index 9045b28bfa9..85dd644b79c 100644 --- a/tests/components/tuya/test_binary_sensor.py +++ b/tests/components/tuya/test_binary_sensor.py @@ -60,7 +60,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) @pytest.mark.parametrize( ("fault_value", "tankfull", "defrost", "wet"), @@ -84,7 +84,7 @@ async def test_bitmap( defrost: str, wet: str, ) -> None: - """Test BITMAP fault sensor on cs_arete_two_12l_dehumidifier_air_purifier.""" + """Test BITMAP fault sensor on cs_zibqa9dutqyaxym2.""" await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) assert hass.states.get("binary_sensor.dehumidifier_tank_full").state == "off" diff --git a/tests/components/tuya/test_climate.py b/tests/components/tuya/test_climate.py index e8aee3f4f96..01fdf469e27 100644 --- a/tests/components/tuya/test_climate.py +++ b/tests/components/tuya/test_climate.py @@ -66,7 +66,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_set_temperature( hass: HomeAssistant, @@ -96,7 +96,7 @@ async def test_set_temperature( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_fan_mode_windspeed( hass: HomeAssistant, @@ -127,7 +127,7 @@ async def test_fan_mode_windspeed( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_fan_mode_no_valid_code( hass: HomeAssistant, @@ -161,7 +161,7 @@ async def test_fan_mode_no_valid_code( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_set_humidity_not_supported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_cover.py b/tests/components/tuya/test_cover.py index 24e43dcccec..20d84878a58 100644 --- a/tests/components/tuya/test_cover.py +++ b/tests/components/tuya/test_cover.py @@ -67,7 +67,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER]) async def test_open_service( @@ -101,7 +101,7 @@ async def test_open_service( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER]) async def test_close_service( @@ -135,7 +135,7 @@ async def test_close_service( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_set_position( hass: HomeAssistant, @@ -168,7 +168,7 @@ async def test_set_position( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @pytest.mark.parametrize( ("percent_control", "percent_state"), @@ -202,7 +202,7 @@ async def test_percent_state_on_cover( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_set_tilt_position_not_supported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_diagnostics.py b/tests/components/tuya/test_diagnostics.py index 2009f117efb..f07c2faa229 100644 --- a/tests/components/tuya/test_diagnostics.py +++ b/tests/components/tuya/test_diagnostics.py @@ -22,7 +22,7 @@ from tests.components.diagnostics import ( from tests.typing import ClientSessionGenerator -@pytest.mark.parametrize("mock_device_code", ["rqbj_gas_sensor"]) +@pytest.mark.parametrize("mock_device_code", ["rqbj_4iqe2hsfyd86kwwc"]) async def test_entry_diagnostics( hass: HomeAssistant, mock_manager: ManagerCompat, @@ -43,7 +43,7 @@ async def test_entry_diagnostics( ) -@pytest.mark.parametrize("mock_device_code", ["rqbj_gas_sensor"]) +@pytest.mark.parametrize("mock_device_code", ["rqbj_4iqe2hsfyd86kwwc"]) async def test_device_diagnostics( hass: HomeAssistant, mock_manager: ManagerCompat, diff --git a/tests/components/tuya/test_humidifier.py b/tests/components/tuya/test_humidifier.py index d4996bcd32a..bd3604b25dd 100644 --- a/tests/components/tuya/test_humidifier.py +++ b/tests/components/tuya/test_humidifier.py @@ -65,7 +65,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_turn_on( hass: HomeAssistant, @@ -92,7 +92,7 @@ async def test_turn_on( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_turn_off( hass: HomeAssistant, @@ -119,7 +119,7 @@ async def test_turn_off( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_set_humidity( hass: HomeAssistant, @@ -149,7 +149,7 @@ async def test_set_humidity( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_turn_on_unsupported( hass: HomeAssistant, @@ -179,7 +179,7 @@ async def test_turn_on_unsupported( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_turn_off_unsupported( hass: HomeAssistant, @@ -209,7 +209,7 @@ async def test_turn_off_unsupported( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_set_humidity_unsupported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_init.py b/tests/components/tuya/test_init.py index 9e9855f9fac..ab96f58ecd0 100644 --- a/tests/components/tuya/test_init.py +++ b/tests/components/tuya/test_init.py @@ -15,7 +15,7 @@ from . import initialize_entry from tests.common import MockConfigEntry -@pytest.mark.parametrize("mock_device_code", ["ydkt_dolceclima_unsupported"]) +@pytest.mark.parametrize("mock_device_code", ["ydkt_jevroj5aguwdbs2e"]) async def test_unsupported_device( hass: HomeAssistant, mock_manager: ManagerCompat, diff --git a/tests/components/tuya/test_light.py b/tests/components/tuya/test_light.py index 0d4706a5563..e3586613876 100644 --- a/tests/components/tuya/test_light.py +++ b/tests/components/tuya/test_light.py @@ -64,7 +64,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["dj_smart_light_bulb"], + ["dj_mki13ie507rlry4r"], ) async def test_turn_on_white( hass: HomeAssistant, @@ -98,7 +98,7 @@ async def test_turn_on_white( @pytest.mark.parametrize( "mock_device_code", - ["dj_smart_light_bulb"], + ["dj_mki13ie507rlry4r"], ) async def test_turn_off( hass: HomeAssistant, diff --git a/tests/components/tuya/test_number.py b/tests/components/tuya/test_number.py index b6c7b1f6de5..f28d6414170 100644 --- a/tests/components/tuya/test_number.py +++ b/tests/components/tuya/test_number.py @@ -59,7 +59,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["mal_alarm_host"], + ["mal_gyitctrjj1kefxp2"], ) async def test_set_value( hass: HomeAssistant, @@ -89,7 +89,7 @@ async def test_set_value( @pytest.mark.parametrize( "mock_device_code", - ["mal_alarm_host"], + ["mal_gyitctrjj1kefxp2"], ) async def test_set_value_no_function( hass: HomeAssistant, diff --git a/tests/components/tuya/test_select.py b/tests/components/tuya/test_select.py index cd1d926ff76..475fab30b90 100644 --- a/tests/components/tuya/test_select.py +++ b/tests/components/tuya/test_select.py @@ -62,7 +62,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_select_option( hass: HomeAssistant, @@ -92,7 +92,7 @@ async def test_select_option( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_select_invalid_option( hass: HomeAssistant, From 1a9cae0f89f0a99819b110d32859abe53fb23512 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 4 Aug 2025 04:17:25 -0400 Subject: [PATCH 227/265] Bump ZHA to 0.0.65 (#149922) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ec08c4f5d9d..facde4ead3a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.64"], + "requirements": ["zha==0.0.65"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index d0df5b416d0..d69ce8368e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3203,7 +3203,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.64 +zha==0.0.65 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa48c183a3b..5e10f227725 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2647,7 +2647,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.64 +zha==0.0.65 # homeassistant.components.zwave_js zwave-js-server-python==0.67.0 From 1a54d566f87b81fe8e337b580467c9daa5c804c7 Mon Sep 17 00:00:00 2001 From: jvmahon Date: Mon, 4 Aug 2025 04:26:11 -0400 Subject: [PATCH 228/265] Apple vendor name update (#149845) --- homeassistant/components/thread/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/thread/discovery.py b/homeassistant/components/thread/discovery.py index d4e47c31dd2..4bd4c6e81f7 100644 --- a/homeassistant/components/thread/discovery.py +++ b/homeassistant/components/thread/discovery.py @@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__) KNOWN_BRANDS: dict[str | None, str] = { "Amazon": "amazon", + "Apple": "apple", "Apple Inc.": "apple", "Aqara": "aqara_gateway", "eero": "eero", From 6fa9d4240113479c7a8b38289df5390d5295698b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Mon, 4 Aug 2025 11:05:32 +0200 Subject: [PATCH 229/265] Airthings ContextVar warning (#149930) --- homeassistant/components/airthings/__init__.py | 7 ++----- homeassistant/components/airthings/coordinator.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index 175fd320062..04c666dc5bc 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -7,21 +7,18 @@ import logging from airthings import Airthings -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SECRET -from .coordinator import AirthingsDataUpdateCoordinator +from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [Platform.SENSOR] SCAN_INTERVAL = timedelta(minutes=6) -type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool: """Set up Airthings from a config entry.""" @@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> async_get_clientsession(hass), ) - coordinator = AirthingsDataUpdateCoordinator(hass, airthings) + coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/airthings/coordinator.py b/homeassistant/components/airthings/coordinator.py index 6172dc0b6ef..9e15e4a0c5d 100644 --- a/homeassistant/components/airthings/coordinator.py +++ b/homeassistant/components/airthings/coordinator.py @@ -5,6 +5,7 @@ import logging from airthings import Airthings, AirthingsDevice, AirthingsError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -13,15 +14,23 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=6) +type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator] + class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]): """Coordinator for Airthings data updates.""" - def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None: + def __init__( + self, + hass: HomeAssistant, + airthings: Airthings, + config_entry: AirthingsConfigEntry, + ) -> None: """Initialize the coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_method=self._update_method, update_interval=SCAN_INTERVAL, From 3dda1685dc74ab4dfa1b4ec63715ad3c0d53f087 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:08:43 +0200 Subject: [PATCH 230/265] Add Tuya snapshots for pc and pir category (#149931) --- tests/components/tuya/__init__.py | 23 ++ .../tuya/fixtures/pc_t2afic7i3v1bwhfp.json | 44 ++++ .../tuya/fixtures/pc_trjopo1vdlt9q1tg.json | 86 ++++++ .../tuya/fixtures/pir_3amxzozho9xp4mkh.json | 44 ++++ .../tuya/fixtures/pir_fcdjzz3s.json | 48 ++++ .../tuya/fixtures/pir_wqz93nrdomectyoz.json | 39 +++ .../tuya/snapshots/test_binary_sensor.ambr | 245 ++++++++++++++++++ .../tuya/snapshots/test_sensor.ambr | 149 +++++++++++ .../tuya/snapshots/test_switch.ambr | 196 ++++++++++++++ 9 files changed, 874 insertions(+) create mode 100644 tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json create mode 100644 tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json create mode 100644 tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json create mode 100644 tests/components/tuya/fixtures/pir_fcdjzz3s.json create mode 100644 tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 040ee1fec2f..371dc4dcfa8 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -123,6 +123,29 @@ DEVICE_MOCKS = { Platform.BINARY_SENSOR, Platform.SENSOR, ], + "pc_t2afic7i3v1bwhfp": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], + "pc_trjopo1vdlt9q1tg": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], + "pir_3amxzozho9xp4mkh": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], + "pir_fcdjzz3s": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], + "pir_wqz93nrdomectyoz": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], "qccdz_7bvgooyjhiua1yyq": [ # https://github.com/home-assistant/core/issues/136207 Platform.SWITCH, diff --git a/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json b/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json new file mode 100644 index 00000000000..4ed7ecf0373 --- /dev/null +++ b/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json @@ -0,0 +1,44 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf2206da15147500969d6e", + "name": "Bubbelbad", + "category": "pc", + "product_id": "t2afic7i3v1bwhfp", + "product_name": "Garden Spike(EU)", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-02-02T15:10:03+00:00", + "create_time": "2022-02-02T15:10:03+00:00", + "update_time": "2022-02-02T15:10:03+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": true, + "switch_2": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json b/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json new file mode 100644 index 00000000000..99929616ec7 --- /dev/null +++ b/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json @@ -0,0 +1,86 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "15727703c4dd5709cd78", + "name": "Terras", + "category": "pc", + "product_id": "trjopo1vdlt9q1tg", + "product_name": "Garden Spike(FR)", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-18T08:42:44+00:00", + "create_time": "2023-01-18T08:42:44+00:00", + "update_time": "2023-01-18T08:42:44+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "switch_2": false, + "countdown_1": 0, + "countdown_2": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json b/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json new file mode 100644 index 00000000000..98843da5614 --- /dev/null +++ b/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json @@ -0,0 +1,44 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "73486068483fda10d633", + "name": "rat trap hedge", + "category": "pir", + "product_id": "3amxzozho9xp4mkh", + "product_name": "Smart Motion Sensor", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2021-11-17T11:36:06+00:00", + "create_time": "2021-11-17T11:36:06+00:00", + "update_time": "2021-11-17T11:36:06+00:00", + "function": {}, + "status_range": { + "pir": { + "type": "Enum", + "value": { + "range": ["pir"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + }, + "temper_alarm": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "pir": "pir", + "battery_state": "low", + "temper_alarm": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pir_fcdjzz3s.json b/tests/components/tuya/fixtures/pir_fcdjzz3s.json new file mode 100644 index 00000000000..65740a4106c --- /dev/null +++ b/tests/components/tuya/fixtures/pir_fcdjzz3s.json @@ -0,0 +1,48 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf445324326cbde7c5rg7b", + "name": "Motion sensor lidl zigbee", + "category": "pir", + "product_id": "fcdjzz3s", + "product_name": "Motion sensor", + "online": false, + "sub": true, + "time_zone": "+01:00", + "active_time": "2022-09-09T07:24:07+00:00", + "create_time": "2022-09-09T07:24:07+00:00", + "update_time": "2022-09-09T07:24:07+00:00", + "function": {}, + "status_range": { + "pir": { + "type": "Enum", + "value": { + "range": ["pir"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temper_alarm": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "pir": "nopir", + "battery_percentage": 85, + "temper_alarm": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json b/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json new file mode 100644 index 00000000000..e4122ee5f9d --- /dev/null +++ b/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json @@ -0,0 +1,39 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "20401777500291cfe3a2", + "name": "PIR outside stairs", + "category": "pir", + "product_id": "wqz93nrdomectyoz", + "product_name": "Smart PIR sensor", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-26T09:56:39+00:00", + "create_time": "2023-01-26T09:56:39+00:00", + "update_time": "2023-01-26T09:56:39+00:00", + "function": {}, + "status_range": { + "pir": { + "type": "Enum", + "value": { + "range": ["pir"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "pir": "pir", + "battery_state": "middle" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_binary_sensor.ambr b/tests/components/tuya/snapshots/test_binary_sensor.ambr index 727e59590a5..bdade20fc9e 100644 --- a/tests/components/tuya/snapshots/test_binary_sensor.ambr +++ b/tests/components/tuya/snapshots/test_binary_sensor.ambr @@ -342,6 +342,251 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.rat_trap_hedge_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.73486068483fda10d633pir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'rat trap hedge Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.rat_trap_hedge_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_tamper-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.rat_trap_hedge_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.73486068483fda10d633temper_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'rat trap hedge Tamper', + }), + 'context': , + 'entity_id': 'binary_sensor.rat_trap_hedge_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf445324326cbde7c5rg7bpir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'Motion sensor lidl zigbee Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_tamper-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.motion_sensor_lidl_zigbee_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf445324326cbde7c5rg7btemper_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Motion sensor lidl zigbee Tamper', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][binary_sensor.pir_outside_stairs_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.pir_outside_stairs_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.20401777500291cfe3a2pir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][binary_sensor.pir_outside_stairs_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'PIR outside stairs Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.pir_outside_stairs_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 882839a6665..740c8db3690 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -1702,6 +1702,155 @@ 'state': '100.0', }) # --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][sensor.rat_trap_hedge_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.rat_trap_hedge_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.73486068483fda10d633battery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][sensor.rat_trap_hedge_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'rat trap hedge Battery state', + }), + 'context': , + 'entity_id': 'sensor.rat_trap_hedge_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][sensor.motion_sensor_lidl_zigbee_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.bf445324326cbde7c5rg7bbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][sensor.motion_sensor_lidl_zigbee_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Motion sensor lidl zigbee Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][sensor.pir_outside_stairs_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pir_outside_stairs_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.20401777500291cfe3a2battery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][sensor.pir_outside_stairs_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'PIR outside stairs Battery state', + }), + 'context': , + 'entity_id': 'sensor.pir_outside_stairs_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'middle', + }) +# --- # name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 2c2325e9ed8..c810649801a 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -869,6 +869,202 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.bubbelbad_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bf2206da15147500969d6eswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Bubbelbad Socket 1', + }), + 'context': , + 'entity_id': 'switch.bubbelbad_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.bubbelbad_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bf2206da15147500969d6eswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Bubbelbad Socket 2', + }), + 'context': , + 'entity_id': 'switch.bubbelbad_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.terras_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.15727703c4dd5709cd78switch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Terras Socket 1', + }), + 'context': , + 'entity_id': 'switch.terras_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.terras_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.15727703c4dd5709cd78switch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Terras Socket 2', + }), + 'context': , + 'entity_id': 'switch.terras_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 83f22497ae0ade469e244d1f9aed618a7239a01e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:09:02 +0200 Subject: [PATCH 231/265] Bump actions/ai-inference from 1.2.3 to 1.2.4 (#149929) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/detect-duplicate-issues.yml | 2 +- .github/workflows/detect-non-english-issues.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/detect-duplicate-issues.yml b/.github/workflows/detect-duplicate-issues.yml index 0facf6fdf77..fd7ed1a38a9 100644 --- a/.github/workflows/detect-duplicate-issues.yml +++ b/.github/workflows/detect-duplicate-issues.yml @@ -231,7 +231,7 @@ jobs: - name: Detect duplicates using AI id: ai_detection if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' - uses: actions/ai-inference@v1.2.3 + uses: actions/ai-inference@v1.2.4 with: model: openai/gpt-4o system-prompt: | diff --git a/.github/workflows/detect-non-english-issues.yml b/.github/workflows/detect-non-english-issues.yml index b1ce58c4b41..eefc896bfcb 100644 --- a/.github/workflows/detect-non-english-issues.yml +++ b/.github/workflows/detect-non-english-issues.yml @@ -57,7 +57,7 @@ jobs: - name: Detect language using AI id: ai_language_detection if: steps.detect_language.outputs.should_continue == 'true' - uses: actions/ai-inference@v1.2.3 + uses: actions/ai-inference@v1.2.4 with: model: openai/gpt-4o-mini system-prompt: | From 46ed8a73fc5a1340ce76816994b9cd08afdbb410 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Aug 2025 11:09:18 +0200 Subject: [PATCH 232/265] Bump automower-ble to 0.2.7 (#149928) --- homeassistant/components/husqvarna_automower_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower_ble/manifest.json b/homeassistant/components/husqvarna_automower_ble/manifest.json index 6eb618cbb04..50430c2a9fa 100644 --- a/homeassistant/components/husqvarna_automower_ble/manifest.json +++ b/homeassistant/components/husqvarna_automower_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble", "iot_class": "local_polling", - "requirements": ["automower-ble==0.2.1"] + "requirements": ["automower-ble==0.2.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index d69ce8368e2..772a7372f31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -560,7 +560,7 @@ aurorapy==0.2.7 autarco==3.1.0 # homeassistant.components.husqvarna_automower_ble -automower-ble==0.2.1 +automower-ble==0.2.7 # homeassistant.components.generic # homeassistant.components.stream diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e10f227725..c8744c9e3c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ aurorapy==0.2.7 autarco==3.1.0 # homeassistant.components.husqvarna_automower_ble -automower-ble==0.2.1 +automower-ble==0.2.7 # homeassistant.components.generic # homeassistant.components.stream From aa8e4c1c1535c61052bfcbcad19a416b1d87fbb6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:11:06 +0200 Subject: [PATCH 233/265] Add Tuya snapshots for sgbj, sp, wfcon and ywbj category (#149933) --- tests/components/tuya/__init__.py | 26 + .../tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json | 69 +++ .../tuya/fixtures/sp_drezasavompxpcgm.json | 182 +++++++ .../tuya/fixtures/sp_rjKXWRohlvOTyLBu.json | 213 ++++++++ .../tuya/fixtures/wfcon_b25mh8sxawsgndck.json | 23 + .../tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json | 65 +++ .../tuya/snapshots/test_binary_sensor.ambr | 49 ++ .../components/tuya/snapshots/test_light.ambr | 114 +++++ .../tuya/snapshots/test_number.ambr | 58 +++ .../tuya/snapshots/test_select.ambr | 466 +++++++++++++++++ .../tuya/snapshots/test_sensor.ambr | 101 ++++ .../components/tuya/snapshots/test_siren.ambr | 49 ++ .../tuya/snapshots/test_switch.ambr | 480 ++++++++++++++++++ 13 files changed, 1895 insertions(+) create mode 100644 tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json create mode 100644 tests/components/tuya/fixtures/sp_drezasavompxpcgm.json create mode 100644 tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json create mode 100644 tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json create mode 100644 tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 371dc4dcfa8..260ba741cb3 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -167,6 +167,24 @@ DEVICE_MOCKS = { # https://github.com/home-assistant/core/issues/148116 Platform.SWITCH, ], + "sgbj_ulv4nnue7gqp0rjk": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.NUMBER, + Platform.SELECT, + Platform.SIREN, + ], + "sp_drezasavompxpcgm": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + Platform.SELECT, + Platform.SWITCH, + ], + "sp_rjKXWRohlvOTyLBu": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + Platform.SELECT, + Platform.SWITCH, + ], "tdq_cq1p0nt0a4rixnex": [ # https://github.com/home-assistant/core/issues/146845 Platform.SELECT, @@ -178,6 +196,9 @@ DEVICE_MOCKS = { Platform.SENSOR, Platform.SWITCH, ], + "wfcon_b25mh8sxawsgndck": [ + # https://github.com/home-assistant/core/issues/149704 + ], "wk_aqoouq7x": [ # https://github.com/home-assistant/core/issues/146263 Platform.CLIMATE, @@ -203,6 +224,11 @@ DEVICE_MOCKS = { # https://github.com/orgs/home-assistant/discussions/288 # unsupported device - no platforms ], + "ywbj_gf9dejhmzffgdyfj": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], "zndb_ze8faryrxr0glqnn": [ # https://github.com/home-assistant/core/issues/138372 Platform.SENSOR, diff --git a/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json b/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json new file mode 100644 index 00000000000..a3068983c87 --- /dev/null +++ b/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json @@ -0,0 +1,69 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf0984adfeffe10d5a3ofd", + "name": "Siren veranda ", + "category": "sgbj", + "product_id": "ulv4nnue7gqp0rjk", + "product_name": "Siren Sensor", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2020-05-05T20:15:05+00:00", + "create_time": "2020-05-05T20:15:05+00:00", + "update_time": "2020-05-05T20:15:05+00:00", + "function": { + "alarm_volume": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high", "mute"] + } + }, + "alarm_time": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 30, + "scale": 0, + "step": 1 + } + }, + "alarm_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "alarm_volume": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high", "mute"] + } + }, + "alarm_time": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 30, + "scale": 0, + "step": 1 + } + }, + "alarm_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "alarm_volume": "middle", + "alarm_time": 10, + "alarm_switch": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json b/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json new file mode 100644 index 00000000000..a6543eac5ea --- /dev/null +++ b/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json @@ -0,0 +1,182 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf7b8e59f8cd49f425mmfm", + "name": "CAM GARAGE", + "category": "sp", + "product_id": "drezasavompxpcgm", + "product_name": "Indoor camera ", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-07-26T13:26:21+00:00", + "create_time": "2021-07-26T13:26:21+00:00", + "update_time": "2021-07-26T13:26:21+00:00", + "function": { + "basic_indicator": { + "type": "Boolean", + "value": {} + }, + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "basic_nightvision": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status_range": { + "basic_indicator": { + "type": "Boolean", + "value": {} + }, + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "basic_nightvision": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_storge": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "sd_status": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 5, + "scale": 1, + "step": 1 + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "movement_detect_pic": { + "type": "Raw", + "value": {} + }, + "sd_format_state": { + "type": "Integer", + "value": { + "unit": "", + "min": -20000, + "max": 20000, + "scale": 1, + "step": 1 + } + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "decibel_upload": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status": { + "basic_indicator": true, + "basic_flip": false, + "basic_osd": false, + "motion_sensitivity": 0, + "basic_nightvision": 0, + "sd_storge": "0|0|0", + "sd_status": 5, + "sd_format": false, + "movement_detect_pic": "**REDACTED**", + "sd_format_state": -20000, + "motion_switch": true, + "decibel_switch": false, + "decibel_sensitivity": 0, + "decibel_upload": 1696802404, + "record_switch": false, + "record_mode": 1 + }, + "set_up": true, + "support_local": false +} diff --git a/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json b/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json new file mode 100644 index 00000000000..9a7bb9f1eca --- /dev/null +++ b/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json @@ -0,0 +1,213 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf9d5b7ea61ea4c9a6rom9", + "name": "CAM PORCH", + "category": "sp", + "product_id": "rjKXWRohlvOTyLBu", + "product_name": "Indoor cam Pan/Tilt ", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2020-07-04T07:41:28+00:00", + "create_time": "2020-07-04T07:41:28+00:00", + "update_time": "2020-07-04T07:41:28+00:00", + "function": { + "basic_indicator": { + "type": "Boolean", + "value": {} + }, + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "motion_timer_setting": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "ptz_stop": { + "type": "Boolean", + "value": {} + }, + "ptz_control": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6", "7", "0"] + } + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "motion_timer_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status_range": { + "basic_indicator": { + "type": "Boolean", + "value": {} + }, + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_storge": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "sd_status": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 5, + "scale": 1, + "step": 1 + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "motion_timer_setting": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "movement_detect_pic": { + "type": "Raw", + "value": {} + }, + "ptz_stop": { + "type": "Boolean", + "value": {} + }, + "sd_format_state": { + "type": "Integer", + "value": { + "unit": "", + "min": -20000, + "max": 20000, + "scale": 1, + "step": 1 + } + }, + "ptz_control": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6", "7", "0"] + } + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "motion_timer_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "decibel_upload": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status": { + "basic_indicator": false, + "basic_flip": false, + "basic_osd": true, + "motion_sensitivity": 2, + "sd_storge": "100|0|100", + "sd_status": 5, + "sd_format": true, + "motion_timer_setting": "00:00|06:00", + "movement_detect_pic": "**REDACTED**", + "ptz_stop": true, + "sd_format_state": 100, + "ptz_control": 6, + "motion_switch": false, + "motion_timer_switch": true, + "decibel_switch": false, + "decibel_sensitivity": 1, + "decibel_upload": 1750049151, + "record_switch": false, + "record_mode": 1 + }, + "set_up": true, + "support_local": false +} diff --git a/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json b/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json new file mode 100644 index 00000000000..2fa798b2f60 --- /dev/null +++ b/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json @@ -0,0 +1,23 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf63312cdd4722555bmsuv", + "name": "ZigBee Gateway", + "category": "wfcon", + "product_id": "b25mh8sxawsgndck", + "product_name": "ZigBee Gateway", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-12-29T15:54:11+00:00", + "create_time": "2020-12-29T15:54:11+00:00", + "update_time": "2020-12-29T15:54:11+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json b/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json new file mode 100644 index 00000000000..f80b0cd5cd1 --- /dev/null +++ b/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json @@ -0,0 +1,65 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "8670375210521cf1349c", + "name": " Smoke detector upstairs ", + "category": "ywbj", + "product_id": "gf9dejhmzffgdyfj", + "product_name": "Smart Smoke Alarm", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2021-11-09T13:21:37+00:00", + "create_time": "2021-11-09T13:21:37+00:00", + "update_time": "2021-11-09T13:21:37+00:00", + "function": { + "muffling": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "smoke_sensor_status": { + "type": "Enum", + "value": { + "range": ["alarm", "normal"] + } + }, + "lifecycle": { + "type": "Boolean", + "value": {} + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "muffling": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "smoke_sensor_status": "normal", + "lifecycle": true, + "battery_state": "low", + "battery_percentage": 16, + "muffling": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_binary_sensor.ambr b/tests/components/tuya/snapshots/test_binary_sensor.ambr index bdade20fc9e..821564240eb 100644 --- a/tests/components/tuya/snapshots/test_binary_sensor.ambr +++ b/tests/components/tuya/snapshots/test_binary_sensor.ambr @@ -636,3 +636,52 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][binary_sensor.smoke_detector_upstairs_smoke-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smoke_detector_upstairs_smoke', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Smoke', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.8670375210521cf1349csmoke_sensor_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][binary_sensor.smoke_detector_upstairs_smoke-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': ' Smoke detector upstairs Smoke', + }), + 'context': , + 'entity_id': 'binary_sensor.smoke_detector_upstairs_smoke', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 06ad884cfa3..26648b0f2cb 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -249,6 +249,120 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][light.cam_garage_indicator_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.cam_garage_indicator_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_indicator', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][light.cam_garage_indicator_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'CAM GARAGE Indicator light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.cam_garage_indicator_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][light.cam_porch_indicator_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.cam_porch_indicator_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_indicator', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][light.cam_porch_indicator_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'CAM PORCH Indicator light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.cam_porch_indicator_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index c6f2bb363b6..abfde43a875 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -293,6 +293,64 @@ 'state': '3.0', }) # --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][number.siren_veranda_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 30.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.siren_veranda_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time', + 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_time', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][number.siren_veranda_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren veranda Time', + 'max': 30.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'number.siren_veranda_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- # name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 4bd058517be..3a30e8dceee 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -361,6 +361,472 @@ 'state': 'cancel', }) # --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][select.siren_veranda_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.siren_veranda_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][select.siren_veranda_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren veranda Volume', + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'context': , + 'entity_id': 'select.siren_veranda_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'middle', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmmotion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_night_vision-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_night_vision', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Night vision', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'basic_nightvision', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_nightvision', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_night_vision-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Night vision', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_night_vision', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmrecord_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_sound_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_sound_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'decibel_sensitivity', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmdecibel_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_sound_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Sound detection sensitivity', + 'options': list([ + '0', + '1', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_sound_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9motion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9record_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_sound_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_sound_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'decibel_sensitivity', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9decibel_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_sound_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Sound detection sensitivity', + 'options': list([ + '0', + '1', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_sound_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 740c8db3690..91cc9e4739b 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -2751,6 +2751,107 @@ 'state': '100.0', }) # --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.8670375210521cf1349cbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': ' Smoke detector upstairs Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.0', + }) +# --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.8670375210521cf1349cbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': ' Smoke detector upstairs Battery state', + }), + 'context': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- # name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_siren.ambr b/tests/components/tuya/snapshots/test_siren.ambr index 7b6afe9dc60..5c46c2bbd19 100644 --- a/tests/components/tuya/snapshots/test_siren.ambr +++ b/tests/components/tuya/snapshots/test_siren.ambr @@ -48,3 +48,52 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][siren.siren_veranda-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'siren', + 'entity_category': None, + 'entity_id': 'siren.siren_veranda', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][siren.siren_veranda-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren veranda ', + 'supported_features': , + }), + 'context': , + 'entity_id': 'siren.siren_veranda', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index c810649801a..57a3fef5c6d 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1161,6 +1161,486 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_flip-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.cam_garage_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Flip', + }), + 'context': , + 'entity_id': 'switch.cam_garage_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_motion_alarm-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.cam_garage_motion_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion alarm', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_alarm', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmmotion_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_motion_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Motion alarm', + }), + 'context': , + 'entity_id': 'switch.cam_garage_motion_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_sound_detection-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.cam_garage_sound_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sound_detection', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmdecibel_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_sound_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Sound detection', + }), + 'context': , + 'entity_id': 'switch.cam_garage_sound_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_time_watermark-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.cam_garage_time_watermark', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time watermark', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_watermark', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_osd', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_time_watermark-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Time watermark', + }), + 'context': , + 'entity_id': 'switch.cam_garage_time_watermark', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_video_recording-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.cam_garage_video_recording', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Video recording', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'video_recording', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmrecord_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_video_recording-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Video recording', + }), + 'context': , + 'entity_id': 'switch.cam_garage_video_recording', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_flip-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.cam_porch_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Flip', + }), + 'context': , + 'entity_id': 'switch.cam_porch_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_motion_alarm-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.cam_porch_motion_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion alarm', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_alarm', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9motion_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_motion_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Motion alarm', + }), + 'context': , + 'entity_id': 'switch.cam_porch_motion_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_sound_detection-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.cam_porch_sound_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sound_detection', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9decibel_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_sound_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Sound detection', + }), + 'context': , + 'entity_id': 'switch.cam_porch_sound_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_time_watermark-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.cam_porch_time_watermark', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time watermark', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_watermark', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_osd', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_time_watermark-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Time watermark', + }), + 'context': , + 'entity_id': 'switch.cam_porch_time_watermark', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_video_recording-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.cam_porch_video_recording', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Video recording', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'video_recording', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9record_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_video_recording-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Video recording', + }), + 'context': , + 'entity_id': 'switch.cam_porch_video_recording', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From e62e3778f3163fa6b002ea35fda3f58de7213a2f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:14:11 +0200 Subject: [PATCH 234/265] Add Tuya snapshots for hps category (#149936) --- tests/components/tuya/__init__.py | 5 + .../tuya/fixtures/hps_2aaelwxk.json | 186 ++++++++++++++++++ .../tuya/snapshots/test_binary_sensor.ambr | 49 +++++ .../tuya/snapshots/test_number.ambr | 176 +++++++++++++++++ 4 files changed, 416 insertions(+) create mode 100644 tests/components/tuya/fixtures/hps_2aaelwxk.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 260ba741cb3..4a9d2acd772 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -92,6 +92,11 @@ DEVICE_MOCKS = { # https://github.com/home-assistant/core/issues/133173 Platform.LIGHT, ], + "hps_2aaelwxk": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.NUMBER, + ], "kg_gbm9ata1zrzaez4a": [ # https://github.com/home-assistant/core/issues/148347 Platform.SWITCH, diff --git a/tests/components/tuya/fixtures/hps_2aaelwxk.json b/tests/components/tuya/fixtures/hps_2aaelwxk.json new file mode 100644 index 00000000000..4e5066e77f4 --- /dev/null +++ b/tests/components/tuya/fixtures/hps_2aaelwxk.json @@ -0,0 +1,186 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf78687ad321a3aeb8a73m", + "name": "Human presence Office", + "category": "hps", + "product_id": "2aaelwxk", + "product_name": "Human presence sensor", + "online": true, + "sub": true, + "time_zone": "+01:00", + "active_time": "2023-12-31T14:40:17+00:00", + "create_time": "2023-12-31T14:40:17+00:00", + "update_time": "2023-12-31T14:40:17+00:00", + "function": { + "sensitivity": { + "type": "Integer", + "value": { + "unit": "x", + "min": 0, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "near_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "far_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "presence_time": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 28800, + "scale": 0, + "step": 1 + } + }, + "motionless_far_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 600, + "scale": 0, + "step": 1 + } + }, + "motionless_sensitivity": { + "type": "Integer", + "value": { + "unit": "x", + "min": 0, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "indicator": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "presence_state": { + "type": "Enum", + "value": { + "range": ["none", "presence"] + } + }, + "sensitivity": { + "type": "Integer", + "value": { + "unit": "x", + "min": 0, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "near_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "far_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "human_motion_state": { + "type": "Enum", + "value": { + "range": ["none", "large_move", "small_move"] + } + }, + "presence_time": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 28800, + "scale": 0, + "step": 1 + } + }, + "motionless_far_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 600, + "scale": 0, + "step": 1 + } + }, + "motionless_sensitivity": { + "type": "Integer", + "value": { + "unit": "x", + "min": 0, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "illuminance_value": { + "type": "Integer", + "value": { + "unit": "lux", + "min": 0, + "max": 6000, + "scale": 0, + "step": 1 + } + }, + "indicator": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "presence_state": "none", + "sensitivity": 3, + "near_detection": 40, + "far_detection": 220, + "human_motion_state": "none", + "presence_time": 30, + "motionless_far_detection": 30, + "motionless_sensitivity": 7, + "illuminance_value": 0, + "indicator": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_binary_sensor.ambr b/tests/components/tuya/snapshots/test_binary_sensor.ambr index 821564240eb..7cb613ebbf2 100644 --- a/tests/components/tuya/snapshots/test_binary_sensor.ambr +++ b/tests/components/tuya/snapshots/test_binary_sensor.ambr @@ -293,6 +293,55 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][binary_sensor.human_presence_office_occupancy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.human_presence_office_occupancy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Occupancy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf78687ad321a3aeb8a73mpresence_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][binary_sensor.human_presence_office_occupancy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'occupancy', + 'friendly_name': 'Human presence Office Occupancy', + }), + 'context': , + 'entity_id': 'binary_sensor.human_presence_office_occupancy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index abfde43a875..9a04b9dd78c 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -116,6 +116,182 @@ 'state': '1.0', }) # --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_far_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1000.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.human_presence_office_far_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Far detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'far_detection', + 'unique_id': 'tuya.bf78687ad321a3aeb8a73mfar_detection', + 'unit_of_measurement': 'cm', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_far_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Human presence Office Far detection', + 'max': 1000.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'cm', + }), + 'context': , + 'entity_id': 'number.human_presence_office_far_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '220.0', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_near_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1000.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.human_presence_office_near_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Near detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'near_detection', + 'unique_id': 'tuya.bf78687ad321a3aeb8a73mnear_detection', + 'unit_of_measurement': 'cm', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_near_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Human presence Office Near detection', + 'max': 1000.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'cm', + }), + 'context': , + 'entity_id': 'number.human_presence_office_near_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.0', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.human_presence_office_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sensitivity', + 'unique_id': 'tuya.bf78687ad321a3aeb8a73msensitivity', + 'unit_of_measurement': 'x', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Human presence Office Sensitivity', + 'max': 10.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'x', + }), + 'context': , + 'entity_id': 'number.human_presence_office_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- # name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 0766edb9c4d0bf9a70cc0fe4ae0290949759145a Mon Sep 17 00:00:00 2001 From: andreimoraru Date: Mon, 4 Aug 2025 12:15:38 +0300 Subject: [PATCH 235/265] Bump yt-dlp to 2025.07.21 (#149916) Co-authored-by: Joostlek --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 20068efccef..db622d21f1a 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.06.09"], + "requirements": ["yt-dlp[default]==2025.07.21"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 772a7372f31..52b2fdb42b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3185,7 +3185,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.06.09 +yt-dlp[default]==2025.07.21 # homeassistant.components.zabbix zabbix-utils==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8744c9e3c8..ffed56c6f3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2632,7 +2632,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.06.09 +yt-dlp[default]==2025.07.21 # homeassistant.components.zamg zamg==0.3.6 From 5837f55205a4dc6979793c746838c7fd9d317091 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:23:58 +0200 Subject: [PATCH 236/265] Add extra Tuya snapshots for cz category (#149938) --- tests/components/tuya/__init__.py | 22 ++ .../tuya/fixtures/cz_0g1fmqh6d5io7lcn.json | 56 ++++ .../tuya/fixtures/cz_cuhokdii7ojyw8k2.json | 56 ++++ .../tuya/fixtures/cz_dntgh2ngvshfxpsz.json | 35 +++ .../tuya/fixtures/cz_hj0a5c7ckzzexu8l.json | 100 ++++++ .../tuya/fixtures/cz_t0a4hwsf8anfsadp.json | 118 +++++++ .../tuya/snapshots/test_select.ambr | 118 +++++++ .../tuya/snapshots/test_sensor.ambr | 174 +++++++++++ .../tuya/snapshots/test_switch.ambr | 293 ++++++++++++++++++ 9 files changed, 972 insertions(+) create mode 100644 tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json create mode 100644 tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json create mode 100644 tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json create mode 100644 tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json create mode 100644 tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 4a9d2acd772..f3525a6e173 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -70,11 +70,33 @@ DEVICE_MOCKS = { Platform.SENSOR, Platform.SWITCH, ], + "cz_0g1fmqh6d5io7lcn": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], "cz_2jxesipczks0kdct": [ # https://github.com/home-assistant/core/issues/147149 Platform.SENSOR, Platform.SWITCH, ], + "cz_cuhokdii7ojyw8k2": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], + "cz_dntgh2ngvshfxpsz": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], + "cz_hj0a5c7ckzzexu8l": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SENSOR, + Platform.SWITCH, + ], + "cz_t0a4hwsf8anfsadp": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SELECT, + Platform.SWITCH, + ], "dj_mki13ie507rlry4r": [ # https://github.com/home-assistant/core/pull/126242 Platform.LIGHT diff --git a/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json b/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json new file mode 100644 index 00000000000..760972e7fb0 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "01155072c4dd573f92b8", + "name": "Apollo light", + "category": "cz", + "product_id": "0g1fmqh6d5io7lcn", + "product_name": "Mini Smart Plug", + "online": false, + "sub": false, + "time_zone": "-07:00", + "active_time": "2024-06-16T17:19:42+00:00", + "create_time": "2024-06-16T17:19:42+00:00", + "update_time": "2024-06-16T17:19:42+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "秒", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "秒", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json b/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json new file mode 100644 index 00000000000..f259ebd7d6c --- /dev/null +++ b/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "53703774d8f15ba9efd3", + "name": "Buitenverlichting", + "category": "cz", + "product_id": "cuhokdii7ojyw8k2", + "product_name": "Smart Plug-EU", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2021-09-02T12:52:48+00:00", + "create_time": "2021-09-02T12:52:48+00:00", + "update_time": "2021-09-02T12:52:48+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json b/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json new file mode 100644 index 00000000000..a92d2d370d0 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json @@ -0,0 +1,35 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf7a2cdaf3ce28d2f7uqnh", + "name": "fakkel veranda ", + "category": "cz", + "product_id": "dntgh2ngvshfxpsz", + "product_name": "Smart Plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-18T07:59:28+00:00", + "create_time": "2023-01-18T07:59:28+00:00", + "update_time": "2023-01-18T07:59:28+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json b/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json new file mode 100644 index 00000000000..0638bb02d1e --- /dev/null +++ b/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json @@ -0,0 +1,100 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "051724052462ab286504", + "name": "droger", + "category": "cz", + "product_id": "hj0a5c7ckzzexu8l", + "product_name": "Smart plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-02-20T21:27:14+00:00", + "create_time": "2020-02-20T21:27:14+00:00", + "update_time": "2020-02-20T21:27:14+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 100, + "cur_current": 2754, + "cur_power": 5935, + "cur_voltage": 2224 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json b/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json new file mode 100644 index 00000000000..b7f913a7153 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json @@ -0,0 +1,118 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf4c0c538bfe408aa9gr2e", + "name": "wallwasher front", + "category": "cz", + "product_id": "t0a4hwsf8anfsadp", + "product_name": "Smart Plug ", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-12-11T15:51:53+00:00", + "create_time": "2022-12-11T15:51:53+00:00", + "update_time": "2022-12-11T15:51:53+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "relay_status": "power_on", + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 3a30e8dceee..943e230b7cd 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -296,6 +296,124 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.wallwasher_front_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2elight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wallwasher front Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.wallwasher_front_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.wallwasher_front_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2erelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wallwasher front Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.wallwasher_front_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 91cc9e4739b..e8b9900185e 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -971,6 +971,180 @@ 'state': '121.7', }) # --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_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.droger_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.051724052462ab286504cur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'droger Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.droger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.754', + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.droger_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.051724052462ab286504cur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'droger Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.droger_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '593.5', + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.droger_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.051724052462ab286504cur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'droger Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.droger_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '222.4', + }) +# --- # name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 57a3fef5c6d..1b90c21bb46 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -434,6 +434,55 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[cz_0g1fmqh6d5io7lcn][switch.apollo_light_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.apollo_light_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.01155072c4dd573f92b8switch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_0g1fmqh6d5io7lcn][switch.apollo_light_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Apollo light Socket 1', + }), + 'context': , + 'entity_id': 'switch.apollo_light_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -532,6 +581,250 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[cz_cuhokdii7ojyw8k2][switch.buitenverlichting_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.buitenverlichting_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.53703774d8f15ba9efd3switch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_cuhokdii7ojyw8k2][switch.buitenverlichting_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Buitenverlichting Socket 1', + }), + 'context': , + 'entity_id': 'switch.buitenverlichting_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[cz_dntgh2ngvshfxpsz][switch.fakkel_veranda_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.fakkel_veranda_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bf7a2cdaf3ce28d2f7uqnhswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_dntgh2ngvshfxpsz][switch.fakkel_veranda_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'fakkel veranda Socket 1', + }), + 'context': , + 'entity_id': 'switch.fakkel_veranda_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][switch.droger_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.droger_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.051724052462ab286504switch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][switch.droger_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'droger Socket 1', + }), + 'context': , + 'entity_id': 'switch.droger_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.wallwasher_front_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2echild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wallwasher front Child lock', + }), + 'context': , + 'entity_id': 'switch.wallwasher_front_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.wallwasher_front_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2eswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'wallwasher front Socket 1', + }), + 'context': , + 'entity_id': 'switch.wallwasher_front_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 377ca04be887a0a7928c61cb54d66be13905cb4c Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:24:51 +0200 Subject: [PATCH 237/265] Update sensor icons in Volvo integration (#149811) --- homeassistant/components/volvo/icons.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/volvo/icons.json b/homeassistant/components/volvo/icons.json index 8e2897c66ad..61f67bcfe04 100644 --- a/homeassistant/components/volvo/icons.json +++ b/homeassistant/components/volvo/icons.json @@ -20,7 +20,11 @@ "default": "mdi:gas-station" }, "charger_connection_status": { - "default": "mdi:ev-plug-ccs2" + "default": "mdi:power-plug-off", + "state": { + "connected": "mdi:power-plug", + "fault": "mdi:flash-alert" + } }, "charging_power": { "default": "mdi:gauge-empty", @@ -44,22 +48,22 @@ } }, "distance_to_empty_battery": { - "default": "mdi:gauge-empty" + "default": "mdi:battery-outline" }, "distance_to_empty_tank": { "default": "mdi:gauge-empty" }, "distance_to_service": { - "default": "mdi:wrench-clock" + "default": "mdi:wrench-check" }, "engine_time_to_service": { - "default": "mdi:wrench-clock" + "default": "mdi:wrench-cog" }, "estimated_charging_time": { "default": "mdi:battery-clock" }, "fuel_amount": { - "default": "mdi:gas-station" + "default": "mdi:fuel" }, "odometer": { "default": "mdi:counter" From bd3fe1d4ad7c6536b5bdf5a55bb5489a48d27b5c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 4 Aug 2025 19:26:14 +1000 Subject: [PATCH 238/265] Fix credit sensor when there are no vehicles in Teslemetry (#149925) --- homeassistant/components/teslemetry/models.py | 2 +- homeassistant/components/teslemetry/sensor.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index 51eed97227e..6d12aa56470 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -28,7 +28,7 @@ class TeslemetryData: vehicles: list[TeslemetryVehicleData] energysites: list[TeslemetryEnergyData] scopes: list[Scope] - stream: TeslemetryStream + stream: TeslemetryStream | None @dataclass diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 1ffe073cc5c..34ee2d4b8e9 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -45,7 +45,7 @@ from .entity import ( TeslemetryVehicleStreamEntity, TeslemetryWallConnectorEntity, ) -from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData +from .models import TeslemetryEnergyData, TeslemetryVehicleData PARALLEL_UPDATES = 0 @@ -1617,11 +1617,12 @@ async def async_setup_entry( if energysite.history_coordinator is not None ) - entities.append( - TeslemetryCreditBalanceSensor( - entry.unique_id or entry.entry_id, entry.runtime_data + if entry.runtime_data.stream is not None: + entities.append( + TeslemetryCreditBalanceSensor( + entry.unique_id or entry.entry_id, entry.runtime_data.stream + ) ) - ) async_add_entities(entities) @@ -1840,12 +1841,12 @@ class TeslemetryCreditBalanceSensor(RestoreSensor): _attr_state_class = SensorStateClass.MEASUREMENT _attr_suggested_display_precision = 0 - def __init__(self, uid: str, data: TeslemetryData) -> None: + def __init__(self, uid: str, stream: TeslemetryStream) -> None: """Initialize common aspects of a Teslemetry entity.""" self._attr_translation_key = "credit_balance" self._attr_unique_id = f"{uid}_credit_balance" - self.stream = data.stream + self.stream = stream async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" From cf14226b02df6bc133d49404ea40816a5184c38e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:02:21 +0200 Subject: [PATCH 239/265] Pass config entry to Fronius coordinator (#149954) --- homeassistant/components/fronius/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 8a3d1ebf04c..cfbdfbcb424 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -106,6 +106,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_logger_{self.host}", + config_entry=self.config_entry, ) await self.logger_coordinator.async_config_entry_first_refresh() @@ -120,6 +121,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_meters_{self.host}", + config_entry=self.config_entry, ) ) @@ -129,6 +131,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_ohmpilot_{self.host}", + config_entry=self.config_entry, ) ) @@ -138,6 +141,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_power_flow_{self.host}", + config_entry=self.config_entry, ) ) @@ -147,6 +151,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_storages_{self.host}", + config_entry=self.config_entry, ) ) @@ -206,6 +211,7 @@ class FroniusSolarNet: logger=_LOGGER, name=_inverter_name, inverter_info=_inverter_info, + config_entry=self.config_entry, ) if self.config_entry.state == ConfigEntryState.LOADED: await _coordinator.async_refresh() From fe2bd8d09e896fbead01306d0d8e36b31647c09f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:02:34 +0200 Subject: [PATCH 240/265] Add Tuya snapshots for ywcgq category (#149948) --- tests/components/tuya/__init__.py | 4 + .../tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json | 139 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index f3525a6e173..4cbe270cdad 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -256,6 +256,10 @@ DEVICE_MOCKS = { Platform.BINARY_SENSOR, Platform.SENSOR, ], + "ywcgq_wtzwyhkev3b4ubns": [ + # https://community.home-assistant.io/t/something-is-wrong-with-tuya-tank-level-sensors-with-the-new-official-integration/689321 + # not (yet) supported + ], "zndb_ze8faryrxr0glqnn": [ # https://github.com/home-assistant/core/issues/138372 Platform.SENSOR, diff --git a/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json b/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json new file mode 100644 index 00000000000..f724ffe164f --- /dev/null +++ b/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json @@ -0,0 +1,139 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf27a4********368f4w", + "name": "Nivel del tanque A", + "category": "ywcgq", + "product_id": "wtzwyhkev3b4ubns", + "product_name": "Tank A Level", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-01-05T10:22:24+00:00", + "create_time": "2024-01-05T10:22:24+00:00", + "update_time": "2024-01-05T10:22:24+00:00", + "function": { + "max_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "mini_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "installation_height": { + "type": "Integer", + "value": { + "unit": "m", + "min": 200, + "max": 2500, + "scale": 3, + "step": 1 + } + }, + "liquid_depth_max": { + "type": "Integer", + "value": { + "unit": "m", + "min": 100, + "max": 2400, + "scale": 3, + "step": 1 + } + } + }, + "status_range": { + "liquid_state": { + "type": "Enum", + "value": { + "range": ["normal", "lower_alarm", "upper_alarm"] + } + }, + "liquid_depth": { + "type": "Integer", + "value": { + "unit": "m", + "min": 0, + "max": 10000, + "scale": 2, + "step": 1 + } + }, + "max_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "mini_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "installation_height": { + "type": "Integer", + "value": { + "unit": "m", + "min": 200, + "max": 2500, + "scale": 3, + "step": 1 + } + }, + "liquid_depth_max": { + "type": "Integer", + "value": { + "unit": "m", + "min": 100, + "max": 2400, + "scale": 3, + "step": 1 + } + }, + "liquid_level_percent": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "liquid_state": "normal", + "liquid_depth": 77, + "max_set": 100, + "mini_set": 10, + "installation_height": 980, + "liquid_depth_max": 140, + "liquid_level_percent": 97 + }, + "set_up": false, + "support_local": true +} From f350a1a1fa8e44f8ee21ab1388b8c8ccca3d81d0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:03:39 +0200 Subject: [PATCH 241/265] Add hassfest check to help with future dependency updates (#149624) --- script/hassfest/requirements.py | 30 +++++++++++++++++-- tests/hassfest/test_requirements.py | 46 ++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 99a1c255e60..9b5334823b9 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -43,6 +43,13 @@ PACKAGE_CHECK_VERSION_RANGE = { "urllib3": "SemVer", "yarl": "SemVer", } +PACKAGE_CHECK_PREPARE_UPDATE: dict[str, int] = { + # In the form dict("dependencyX": n+1) + # - dependencyX should be the name of the referenced dependency + # - current major version +1 + # Pandas will only fully support Python 3.14 in v3. + "pandas": 3, +} PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # In the form dict("domain": {"package": {"dependency1", "dependency2"}}) # - domain is the integration domain @@ -53,6 +60,10 @@ PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # geocachingapi > reverse_geocode > scipy > numpy "scipy": {"numpy"} }, + "noaa_tides": { + # https://github.com/GClunies/noaa_coops/pull/69 + "noaa-coops": {"pandas"} + }, } PACKAGE_REGEX = re.compile( @@ -568,7 +579,7 @@ def check_dependency_version_range( version == "Any" or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None or all( - _is_dependency_version_range_valid(version_part, convention) + _is_dependency_version_range_valid(version_part, convention, pkg) for version_part in version.split(";", 1)[0].split(",") ) ): @@ -582,22 +593,35 @@ def check_dependency_version_range( return False -def _is_dependency_version_range_valid(version_part: str, convention: str) -> bool: +def _is_dependency_version_range_valid( + version_part: str, convention: str, pkg: str | None = None +) -> bool: + prepare_update = PACKAGE_CHECK_PREPARE_UPDATE.get(pkg) if pkg else None version_match = PIP_VERSION_RANGE_SEPARATOR.match(version_part.strip()) operator = version_match.group(1) version = version_match.group(2) + awesome = AwesomeVersion(version) if operator in (">", ">=", "!="): # Lower version binding and version exclusion are fine return True + if prepare_update is not None: + if operator in ("==", "~="): + # Only current major version allowed which prevents updates to the next one + return False + # Allow upper constraints for major version + 1 + if operator == "<" and awesome.section(0) < prepare_update + 1: + return False + if operator == "<=" and awesome.section(0) < prepare_update: + return False + if convention == "SemVer": if operator == "==": # Explicit version with wildcard is allowed only on major version # e.g. ==1.* is allowed, but ==1.2.* is not return version.endswith(".*") and version.count(".") == 1 - awesome = AwesomeVersion(version) if operator in ("<", "<="): # Upper version binding only allowed on major version # e.g. <=3 is allowed, but <=3.1 is not diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index b9259596c65..dcd35a3aca7 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -1,11 +1,17 @@ """Tests for hassfest requirements.""" from pathlib import Path +from unittest.mock import patch import pytest from script.hassfest.model import Config, Integration -from script.hassfest.requirements import validate_requirements_format +from script.hassfest.requirements import ( + PACKAGE_CHECK_PREPARE_UPDATE, + PACKAGE_CHECK_VERSION_RANGE, + check_dependency_version_range, + validate_requirements_format, +) @pytest.fixture @@ -105,3 +111,41 @@ def test_validate_requirements_format_github_custom(integration: Integration) -> integration.path = Path("") assert validate_requirements_format(integration) assert len(integration.errors) == 0 + + +@pytest.mark.parametrize( + ("version", "result"), + [ + (">2", True), + (">=2.0", True), + (">=2.0,<4", True), + ("<4", True), + ("<=3.0", True), + (">=2.0,<4;python_version<'3.14'", True), + ("<3", False), + ("==2.*", False), + ("~=2.0", False), + ("<=2.100", False), + (">2,<3", False), + (">=2.0,<3", False), + (">=2.0,<3;python_version<'3.14'", False), + ], +) +def test_dependency_version_range_prepare_update( + version: str, result: bool, integration: Integration +) -> None: + """Test dependency version range check for prepare update is working correctly.""" + with ( + patch.dict(PACKAGE_CHECK_VERSION_RANGE, {"numpy-test": "SemVer"}, clear=True), + patch.dict(PACKAGE_CHECK_PREPARE_UPDATE, {"numpy-test": 3}, clear=True), + ): + assert ( + check_dependency_version_range( + integration, + "test", + pkg="numpy-test", + version=version, + package_exceptions=set(), + ) + == result + ) From 8d8383e1c16d37c07f5c76f3fbb0bc23d4c4bb3e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:07:25 +0200 Subject: [PATCH 242/265] Add extra Tuya snapshots for dc and dj category (lights) (#149940) --- tests/components/tuya/__init__.py | 106 +- .../tuya/fixtures/dc_l3bpgg8ibsagon4x.json | 149 ++ .../tuya/fixtures/dj_8szt7whdvwpmxglk.json | 495 +++++ .../tuya/fixtures/dj_8y0aquaa8v6tho8w.json | 338 +++ .../tuya/fixtures/dj_baf9tt9lb8t5uc7z.json | 77 + .../tuya/fixtures/dj_d4g0fbsoaal841o6.json | 377 ++++ .../tuya/fixtures/dj_djnozmdyqyriow8z.json | 484 +++++ .../tuya/fixtures/dj_ekwolitfjhxn55js.json | 559 +++++ .../tuya/fixtures/dj_fuupmcr2mb1odkja.json | 338 +++ .../tuya/fixtures/dj_hp6orhaqm6as3jnv.json | 510 +++++ .../tuya/fixtures/dj_hpc8ddyfv85haxa7.json | 156 ++ .../tuya/fixtures/dj_iayz2jmtlipjnxj7.json | 529 +++++ .../tuya/fixtures/dj_idnfq7xbx8qewyoa.json | 523 +++++ .../tuya/fixtures/dj_ilddqqih3tucdk68.json | 77 + .../tuya/fixtures/dj_j1bgp31cffutizub.json | 434 ++++ .../tuya/fixtures/dj_lmnt3uyltk1xffrt.json | 77 + .../tuya/fixtures/dj_nbumqpv8vz61enji.json | 559 +++++ .../tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json | 77 + .../components/tuya/fixtures/dj_oe0cpnjg.json | 226 ++ .../components/tuya/fixtures/dj_riwp3k79.json | 402 ++++ .../tuya/fixtures/dj_tmsloaroqavbucgn.json | 377 ++++ .../tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json | 335 +++ .../tuya/fixtures/dj_vqwcnabamzrc2kab.json | 532 +++++ .../tuya/fixtures/dj_xokdfs6kh5ednakk.json | 377 ++++ .../tuya/fixtures/dj_zakhnlpdiu0ycdxn.json | 77 + .../tuya/fixtures/dj_zav1pa32pyxray78.json | 322 +++ .../tuya/fixtures/dj_zputiamzanuk6yky.json | 413 ++++ .../components/tuya/snapshots/test_light.ambr | 1903 +++++++++++++++++ 28 files changed, 10828 insertions(+), 1 deletion(-) create mode 100644 tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json create mode 100644 tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json create mode 100644 tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json create mode 100644 tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json create mode 100644 tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json create mode 100644 tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json create mode 100644 tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json create mode 100644 tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json create mode 100644 tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json create mode 100644 tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json create mode 100644 tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json create mode 100644 tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json create mode 100644 tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json create mode 100644 tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json create mode 100644 tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json create mode 100644 tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json create mode 100644 tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json create mode 100644 tests/components/tuya/fixtures/dj_oe0cpnjg.json create mode 100644 tests/components/tuya/fixtures/dj_riwp3k79.json create mode 100644 tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json create mode 100644 tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json create mode 100644 tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json create mode 100644 tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json create mode 100644 tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json create mode 100644 tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json create mode 100644 tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 4cbe270cdad..7d6cd32959c 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -97,9 +97,113 @@ DEVICE_MOCKS = { Platform.SELECT, Platform.SWITCH, ], + "dc_l3bpgg8ibsagon4x": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_8szt7whdvwpmxglk": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_8y0aquaa8v6tho8w": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_baf9tt9lb8t5uc7z": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_d4g0fbsoaal841o6": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_djnozmdyqyriow8z": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_ekwolitfjhxn55js": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_fuupmcr2mb1odkja": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_hp6orhaqm6as3jnv": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_hpc8ddyfv85haxa7": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_iayz2jmtlipjnxj7": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_idnfq7xbx8qewyoa": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_ilddqqih3tucdk68": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_j1bgp31cffutizub": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_lmnt3uyltk1xffrt": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], "dj_mki13ie507rlry4r": [ # https://github.com/home-assistant/core/pull/126242 - Platform.LIGHT + Platform.LIGHT, + ], + "dj_nbumqpv8vz61enji": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_nlxvjzy1hoeiqsg6": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_oe0cpnjg": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_riwp3k79": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_tmsloaroqavbucgn": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_ufq2xwuzd4nb0qdr": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_vqwcnabamzrc2kab": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_xokdfs6kh5ednakk": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_zakhnlpdiu0ycdxn": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_zav1pa32pyxray78": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_zputiamzanuk6yky": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, ], "dlq_0tnvg2xaisqdadcf": [ # https://github.com/home-assistant/core/issues/102769 diff --git a/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json b/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json new file mode 100644 index 00000000000..b3759178618 --- /dev/null +++ b/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json @@ -0,0 +1,149 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfd9f45c6b882c9f46dxfc", + "name": "LSC Party String Light RGBIC+CCT ", + "category": "dc", + "product_id": "l3bpgg8ibsagon4x", + "product_name": "LSC Party String Light RGBIC+CCT ", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2024-07-18T20:38:14+00:00", + "create_time": "2024-07-18T20:38:14+00:00", + "update_time": "2024-07-18T20:38:14+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "music_data": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "colour", + "bright_value": 1000, + "temp_value": 0, + "colour_data": { + "h": 229, + "s": 1000, + "v": 1000 + } + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json b/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json new file mode 100644 index 00000000000..6cd0ca55379 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json @@ -0,0 +1,495 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "eb10549aadfc74b7c8q2ti", + "name": "Porch light E", + "category": "dj", + "product_id": "8szt7whdvwpmxglk", + "product_name": "Smart Light Bulb", + "online": true, + "sub": false, + "time_zone": "-06:00", + "active_time": "2024-06-19T00:38:29+00:00", + "create_time": "2024-06-19T00:38:29+00:00", + "update_time": "2024-06-19T00:38:29+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "colour_data_v2": { + "h": 245, + "s": 780, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 1000, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json b/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json new file mode 100644 index 00000000000..ec8f6a0a4d5 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json @@ -0,0 +1,338 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf71858c3d27943679dsx9", + "name": "dressoir spot", + "category": "dj", + "product_id": "8y0aquaa8v6tho8w", + "product_name": "A60 Clear", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-18T07:49:40+00:00", + "create_time": "2023-01-18T07:49:40+00:00", + "update_time": "2023-01-18T07:49:40+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "remote_switch": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json b/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json new file mode 100644 index 00000000000..211c0bc12cf --- /dev/null +++ b/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "40611462e09806c73134", + "name": "Pokerlamp 2", + "category": "dj", + "product_id": "baf9tt9lb8t5uc7z", + "product_name": "LED SMART", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2021-10-30T17:22:29+00:00", + "create_time": "2021-10-30T17:22:29+00:00", + "update_time": "2021-10-30T17:22:29+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": true, + "bright_value": 45, + "temp_value": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json b/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json new file mode 100644 index 00000000000..22650f7ae37 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json @@ -0,0 +1,377 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf671413db4cee1f9bqdcx", + "name": "WC D1", + "category": "dj", + "product_id": "d4g0fbsoaal841o6", + "product_name": "A60 GOLD", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-06-30T11:36:31+00:00", + "create_time": "2021-06-30T11:36:31+00:00", + "update_time": "2021-06-30T11:36:31+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json b/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json new file mode 100644 index 00000000000..67df13c674f --- /dev/null +++ b/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json @@ -0,0 +1,484 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf8885f3d18a73e395bfac", + "name": "Fakkel 8", + "category": "dj", + "product_id": "djnozmdyqyriow8z", + "product_name": "Candle RGB-CCT", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-06-30T12:13:49+00:00", + "create_time": "2021-06-30T12:13:49+00:00", + "update_time": "2021-06-30T12:13:49+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 280, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 56, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 8, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 120, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 61, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 174, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 275, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json b/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json new file mode 100644 index 00000000000..90cad22fd09 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json @@ -0,0 +1,559 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfb99bba00c9c90ba8gzgl", + "name": "ab6", + "category": "dj", + "product_id": "ekwolitfjhxn55js", + "product_name": "LSC Smart Connect GU10 RGB+CCT", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-10-30T21:26:33+00:00", + "create_time": "2024-10-30T21:26:33+00:00", + "update_time": "2024-10-30T21:26:33+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 3, + "s": 994, + "v": 443 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAAAPoA+gD6AAA", + "do_not_disturb": false, + "remote_switch": true, + "cycle_timing": "AAAA", + "random_timing": "AAAA" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json b/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json new file mode 100644 index 00000000000..5b189b6a3e4 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json @@ -0,0 +1,338 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf0914a82b06ecf151xsf5", + "name": "Slaapkamer", + "category": "dj", + "product_id": "fuupmcr2mb1odkja", + "product_name": "ST64 Clear", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-28T01:25:04+00:00", + "create_time": "2023-01-28T01:25:04+00:00", + "update_time": "2023-01-28T01:25:04+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json b/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json new file mode 100644 index 00000000000..e8166a192dc --- /dev/null +++ b/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json @@ -0,0 +1,510 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "00450321483fda81c529", + "name": "Master bedroom TV lights", + "category": "dj", + "product_id": "hp6orhaqm6as3jnv", + "product_name": "LED Strip Lights", + "online": true, + "sub": false, + "time_zone": "-07:00", + "active_time": "2024-06-19T03:35:54+00:00", + "create_time": "2024-06-19T03:35:54+00:00", + "update_time": "2024-06-19T03:35:54+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "scene_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_1": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_2": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_3": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_4": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "scene_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_1": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_2": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_3": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_4": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "colour", + "bright_value": 96, + "temp_value": 223, + "colour_data": { + "h": 27.0, + "s": 255.0, + "v": 52.0 + }, + "scene_data": { + "h": 16.0, + "s": 255.0, + "v": 210.9 + }, + "flash_scene_1": { + "bright": 255, + "frequency": 80, + "hsv": [ + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + } + ], + "temperature": 255 + }, + "flash_scene_2": { + "bright": 255, + "frequency": 128, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 240.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + } + ], + "temperature": 255 + }, + "flash_scene_3": { + "bright": 255, + "frequency": 80, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + } + ], + "temperature": 255 + }, + "flash_scene_4": { + "bright": 255, + "frequency": 5, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 60.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 300.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 240.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + } + ], + "temperature": 255 + } + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json b/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json new file mode 100644 index 00000000000..893aafa3759 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json @@ -0,0 +1,156 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "63362034840d8eb9029f", + "name": "Garage", + "category": "dj", + "product_id": "hpc8ddyfv85haxa7", + "product_name": "RGB Smart Plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-12-21T14:43:57+00:00", + "create_time": "2020-12-21T14:43:57+00:00", + "update_time": "2020-12-21T14:43:57+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "switch_1": { + "type": "Boolean", + "value": {} + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value": 255, + "temp_value": 255, + "colour_data_v2": { + "h": 16384, + "s": 65280, + "v": 65535 + }, + "switch_1": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json b/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json new file mode 100644 index 00000000000..f9062d9146d --- /dev/null +++ b/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json @@ -0,0 +1,529 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf0fc1d7d4caa71a59us7c", + "name": "LED Porch 2", + "category": "dj", + "product_id": "iayz2jmtlipjnxj7", + "product_name": "LED Strip RGB+W", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-06-07T10:55:19+00:00", + "create_time": "2021-06-07T10:55:19+00:00", + "update_time": "2021-06-07T10:55:19+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 839, + "colour_data_v2": { + "h": 13, + "s": 992, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 8, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 120, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 61, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 174, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 275, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json b/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json new file mode 100644 index 00000000000..295157d8370 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json @@ -0,0 +1,523 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf599f5cffe1a5985depyk", + "name": "AB1", + "category": "dj", + "product_id": "idnfq7xbx8qewyoa", + "product_name": "Smart Lamp", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-08-16T12:51:52+00:00", + "create_time": "2021-08-16T12:51:52+00:00", + "update_time": "2021-08-16T12:51:52+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "scene", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "colour_data_v2": { + "h": 6, + "s": 978, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 6, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 120, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 61, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 174, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 275, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json b/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json new file mode 100644 index 00000000000..1181b650f3e --- /dev/null +++ b/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "84178216d8f15be52dc4", + "name": "Ieskas", + "category": "dj", + "product_id": "ilddqqih3tucdk68", + "product_name": "LED SMART", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-05-28T20:07:13+00:00", + "create_time": "2025-05-28T20:07:13+00:00", + "update_time": "2025-05-28T20:07:13+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": true, + "bright_value": 255, + "temp_value": 158 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json b/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json new file mode 100644 index 00000000000..d95179c921f --- /dev/null +++ b/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json @@ -0,0 +1,434 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfe49d7b6cd80536efdldi", + "name": "Ceiling Portal", + "category": "dj", + "product_id": "j1bgp31cffutizub", + "product_name": "LSC Smart Ceiling Light", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-01-31T12:27:35+00:00", + "create_time": "2022-01-31T12:27:35+00:00", + "update_time": "2022-01-31T12:27:35+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 950, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json b/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json new file mode 100644 index 00000000000..93a802a7ee3 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "07608286600194e94248", + "name": "DirectietKamer", + "category": "dj", + "product_id": "lmnt3uyltk1xffrt", + "product_name": "LED SMART", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-05-28T20:00:48+00:00", + "create_time": "2025-05-28T20:00:48+00:00", + "update_time": "2025-05-28T20:00:48+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 255, + "temp_value": 255 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json b/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json new file mode 100644 index 00000000000..bc919dd92d2 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json @@ -0,0 +1,559 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf77c04cbd6a52a7be16ll", + "name": "b2", + "category": "dj", + "product_id": "nbumqpv8vz61enji", + "product_name": "LSC smart GU10", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-10-30T21:35:27+00:00", + "create_time": "2024-10-30T21:35:27+00:00", + "update_time": "2024-10-30T21:35:27+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 10, + "temp_value_v2": 150, + "colour_data_v2": { + "h": 119, + "s": 935, + "v": 132 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAAAPoA+gD6ACW", + "do_not_disturb": true, + "remote_switch": true, + "cycle_timing": "AAAA", + "random_timing": "AAAA" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json b/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json new file mode 100644 index 00000000000..c519f1aa593 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "40350105dc4f229a464e", + "name": "hall 💡 ", + "category": "dj", + "product_id": "nlxvjzy1hoeiqsg6", + "product_name": "LED SMART", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-06-23T21:37:40+00:00", + "create_time": "2020-06-23T21:37:40+00:00", + "update_time": "2020-06-23T21:37:40+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 135, + "temp_value": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_oe0cpnjg.json b/tests/components/tuya/fixtures/dj_oe0cpnjg.json new file mode 100644 index 00000000000..646ce8a93d7 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_oe0cpnjg.json @@ -0,0 +1,226 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf8d8af3ddfe75b0195r0h", + "name": "Front right Lighting trap", + "category": "dj", + "product_id": "oe0cpnjg", + "product_name": "Smart Lighting", + "online": true, + "sub": true, + "time_zone": "+01:00", + "active_time": "2023-10-03T13:23:20+00:00", + "create_time": "2023-10-03T13:23:20+00:00", + "update_time": "2023-10-03T13:23:20+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 985, + "colour_data_v2": "", + "music_data": "" + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_riwp3k79.json b/tests/components/tuya/fixtures/dj_riwp3k79.json new file mode 100644 index 00000000000..f1a3579e660 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_riwp3k79.json @@ -0,0 +1,402 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf46b2b81ca41ce0c1xpsw", + "name": "LED KEUKEN 2", + "category": "dj", + "product_id": "riwp3k79", + "product_name": "atmosphere", + "online": true, + "sub": true, + "time_zone": "+08:00", + "active_time": "2020-12-29T16:16:11+00:00", + "create_time": "2020-12-29T16:16:11+00:00", + "update_time": "2020-12-29T16:16:11+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 27, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 6, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 100, + "unit_switch_duration": 100, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 100, + "unit_switch_duration": 100, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json b/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json new file mode 100644 index 00000000000..20c91ad7739 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json @@ -0,0 +1,377 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf252b8ee16b2e78bdoxlp", + "name": "Pokerlamp 1", + "category": "dj", + "product_id": "tmsloaroqavbucgn", + "product_name": "G95-Filament", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-06-29T16:12:54+00:00", + "create_time": "2021-06-29T16:12:54+00:00", + "update_time": "2021-06-29T16:12:54+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 400, + "temp_value_v2": 1000, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json b/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json new file mode 100644 index 00000000000..7ea5905411d --- /dev/null +++ b/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json @@ -0,0 +1,335 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf8edbd51a52c01a4bfgqf", + "name": "Sjiethoes", + "category": "dj", + "product_id": "ufq2xwuzd4nb0qdr", + "product_name": "Smart Ceiling Lamp", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-04-29T09:40:53+00:00", + "create_time": "2025-04-29T09:40:53+00:00", + "update_time": "2025-04-29T09:40:53+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 46, + "s": 1000, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json b/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json new file mode 100644 index 00000000000..4d6749ea0b4 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json @@ -0,0 +1,532 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfd56f4718874ee8830xdw", + "name": "Strip 2", + "category": "dj", + "product_id": "vqwcnabamzrc2kab", + "product_name": "Light Strip-RGBCW ", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-10-22T13:55:55+00:00", + "create_time": "2021-10-22T13:55:55+00:00", + "update_time": "2021-10-22T13:55:55+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "colour_data_v2": { + "h": 218, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 8, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 120, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 61, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 174, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 275, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json b/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json new file mode 100644 index 00000000000..cce66d90b0c --- /dev/null +++ b/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json @@ -0,0 +1,377 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfc1ef4da4accc0731oggw", + "name": "ERKER 1-Gold ", + "category": "dj", + "product_id": "xokdfs6kh5ednakk", + "product_name": "LSC-G125-Gold ", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-01-30T22:02:31+00:00", + "create_time": "2022-01-30T22:02:31+00:00", + "update_time": "2022-01-30T22:02:31+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json b/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json new file mode 100644 index 00000000000..d1c23663144 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "03010850c44f33966362", + "name": "Stoel", + "category": "dj", + "product_id": "zakhnlpdiu0ycdxn", + "product_name": "LED SMART", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-08-10T18:55:12+00:00", + "create_time": "2023-08-10T18:55:12+00:00", + "update_time": "2023-08-10T18:55:12+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 71, + "temp_value": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json b/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json new file mode 100644 index 00000000000..624f7fb4347 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json @@ -0,0 +1,322 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "500425642462ab50909b", + "name": "Gengske 💡 ", + "category": "dj", + "product_id": "zav1pa32pyxray78", + "product_name": "Ceiling Light RGBTW", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-05-28T20:00:48+00:00", + "create_time": "2025-05-28T20:00:48+00:00", + "update_time": "2025-05-28T20:00:48+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 380, + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 102 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json b/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json new file mode 100644 index 00000000000..cede2b65682 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json @@ -0,0 +1,413 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf74164049de868395pbci", + "name": "Floodlight", + "category": "dj", + "product_id": "zputiamzanuk6yky", + "product_name": "LSC Floodlight", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-06-09T08:14:06+00:00", + "create_time": "2025-06-09T08:14:06+00:00", + "update_time": "2025-06-09T08:14:06+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 1000, + "colour_data_v2": { + "h": 295, + "s": 920, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 1000, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAGwG/A+gD6APo", + "do_not_disturb": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 26648b0f2cb..b5dca58f8e7 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -56,6 +56,1141 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[dc_l3bpgg8ibsagon4x][light.lsc_party_string_light_rgbic_cct-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.lsc_party_string_light_rgbic_cct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfd9f45c6b882c9f46dxfcswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dc_l3bpgg8ibsagon4x][light.lsc_party_string_light_rgbic_cct-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LSC Party String Light RGBIC+CCT ', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.lsc_party_string_light_rgbic_cct', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_8szt7whdvwpmxglk][light.porch_light_e-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.porch_light_e', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.eb10549aadfc74b7c8q2tiswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_8szt7whdvwpmxglk][light.porch_light_e-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Porch light E', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.porch_light_e', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_8y0aquaa8v6tho8w][light.dressoir_spot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dressoir_spot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf71858c3d27943679dsx9switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_8y0aquaa8v6tho8w][light.dressoir_spot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'dressoir spot', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.dressoir_spot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_baf9tt9lb8t5uc7z][light.pokerlamp_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.pokerlamp_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.40611462e09806c73134switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_baf9tt9lb8t5uc7z][light.pokerlamp_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pokerlamp 2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.pokerlamp_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_d4g0fbsoaal841o6][light.wc_d1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.wc_d1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf671413db4cee1f9bqdcxswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_d4g0fbsoaal841o6][light.wc_d1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'WC D1', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.wc_d1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_djnozmdyqyriow8z][light.fakkel_8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.fakkel_8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf8885f3d18a73e395bfacswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_djnozmdyqyriow8z][light.fakkel_8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 70, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'Fakkel 8', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.fakkel_8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_ekwolitfjhxn55js][light.ab6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ab6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfb99bba00c9c90ba8gzglswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_ekwolitfjhxn55js][light.ab6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ab6', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.ab6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_fuupmcr2mb1odkja][light.slaapkamer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.slaapkamer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf0914a82b06ecf151xsf5switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_fuupmcr2mb1odkja][light.slaapkamer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Slaapkamer', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.slaapkamer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_hp6orhaqm6as3jnv][light.master_bedroom_tv_lights-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.master_bedroom_tv_lights', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.00450321483fda81c529switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_hp6orhaqm6as3jnv][light.master_bedroom_tv_lights-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 51, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Master bedroom TV lights', + 'hs_color': tuple( + 26.072, + 100.0, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 111, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.632, + 0.358, + ), + }), + 'context': , + 'entity_id': 'light.master_bedroom_tv_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.garage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.63362034840d8eb9029fswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Garage', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.garage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage_light_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.garage_light_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_light', + 'unique_id': 'tuya.63362034840d8eb9029fswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage_light_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'Garage Light 1', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.garage_light_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_iayz2jmtlipjnxj7][light.led_porch_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.led_porch_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf0fc1d7d4caa71a59us7cswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_iayz2jmtlipjnxj7][light.led_porch_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LED Porch 2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.led_porch_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_idnfq7xbx8qewyoa][light.ab1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ab1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf599f5cffe1a5985depykswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_idnfq7xbx8qewyoa][light.ab1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'AB1', + 'hs_color': tuple( + 6.0, + 97.8, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 31, + 6, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.693, + 0.304, + ), + }), + 'context': , + 'entity_id': 'light.ab1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_ilddqqih3tucdk68][light.ieskas-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ieskas', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.84178216d8f15be52dc4switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_ilddqqih3tucdk68][light.ieskas-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 285, + 'color_temp_kelvin': 3508, + 'friendly_name': 'Ieskas', + 'hs_color': tuple( + 27.165, + 44.6, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 193, + 141, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.453, + 0.374, + ), + }), + 'context': , + 'entity_id': 'light.ieskas', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_j1bgp31cffutizub][light.ceiling_portal-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ceiling_portal', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfe49d7b6cd80536efdldiswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_j1bgp31cffutizub][light.ceiling_portal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Ceiling Portal', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.ceiling_portal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_lmnt3uyltk1xffrt][light.directietkamer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.directietkamer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.07608286600194e94248switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_lmnt3uyltk1xffrt][light.directietkamer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'DirectietKamer', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.directietkamer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -119,6 +1254,774 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[dj_nbumqpv8vz61enji][light.b2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.b2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf77c04cbd6a52a7be16llswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_nbumqpv8vz61enji][light.b2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'b2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.b2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_nlxvjzy1hoeiqsg6][light.hall-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hall', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.40350105dc4f229a464eswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_nlxvjzy1hoeiqsg6][light.hall-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'hall 💡 ', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.hall', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_oe0cpnjg][light.front_right_lighting_trap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.front_right_lighting_trap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf8d8af3ddfe75b0195r0hswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_oe0cpnjg][light.front_right_lighting_trap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Front right Lighting trap', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.front_right_lighting_trap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_riwp3k79][light.led_keuken_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.led_keuken_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf46b2b81ca41ce0c1xpswswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_riwp3k79][light.led_keuken_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'LED KEUKEN 2', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.led_keuken_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_tmsloaroqavbucgn][light.pokerlamp_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.pokerlamp_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf252b8ee16b2e78bdoxlpswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_tmsloaroqavbucgn][light.pokerlamp_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pokerlamp 1', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.pokerlamp_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_ufq2xwuzd4nb0qdr][light.sjiethoes-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.sjiethoes', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf8edbd51a52c01a4bfgqfswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_ufq2xwuzd4nb0qdr][light.sjiethoes-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sjiethoes', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.sjiethoes', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_vqwcnabamzrc2kab][light.strip_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.strip_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfd56f4718874ee8830xdwswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_vqwcnabamzrc2kab][light.strip_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Strip 2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.strip_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_xokdfs6kh5ednakk][light.erker_1_gold-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.erker_1_gold', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfc1ef4da4accc0731oggwswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_xokdfs6kh5ednakk][light.erker_1_gold-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'ERKER 1-Gold ', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.erker_1_gold', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_zakhnlpdiu0ycdxn][light.stoel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.stoel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.03010850c44f33966362switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_zakhnlpdiu0ycdxn][light.stoel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Stoel', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.stoel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_zav1pa32pyxray78][light.gengske-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.gengske', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.500425642462ab50909bswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_zav1pa32pyxray78][light.gengske-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Gengske 💡 ', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.gengske', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_zputiamzanuk6yky][light.floodlight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.floodlight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf74164049de868395pbciswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_zputiamzanuk6yky][light.floodlight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Floodlight', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.floodlight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From c1ccfee7cc79f9ef8b6731d006c481253a5820b2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:08:03 +0200 Subject: [PATCH 243/265] Pass config entry to AsusWRT coordinator (#149953) --- homeassistant/components/asuswrt/router.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index a34f191b7a7..3cf8d2e863d 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping from datetime import datetime, timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from pyasuswrt import AsusWrtError @@ -40,6 +40,9 @@ from .const import ( SENSORS_CONNECTED_DEVICE, ) +if TYPE_CHECKING: + from . import AsusWrtConfigEntry + CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] SCAN_INTERVAL = timedelta(seconds=30) @@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__) class AsusWrtSensorDataHandler: """Data handler for AsusWrt sensor.""" - def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None: + def __init__( + self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry + ) -> None: """Initialize a AsusWrt sensor data handler.""" self._hass = hass self._api = api + self._entry = entry self._connected_devices = 0 async def _get_connected_devices(self) -> dict[str, int]: @@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler: update_method=method, # Polling interval. Will only be polled if there are subscribers. update_interval=SCAN_INTERVAL if should_poll else None, + config_entry=self._entry, ) await coordinator.async_refresh() @@ -321,7 +328,9 @@ class AsusWrtRouter: if self._sensors_data_handler: return - self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api) + self._sensors_data_handler = AsusWrtSensorDataHandler( + self.hass, self._api, self._entry + ) self._sensors_data_handler.update_device_count(self._connected_devices) sensors_types = await self._api.async_get_available_sensors() From afffe0b08bf450494f242e709529ac052ad19fce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Aug 2025 12:20:30 +0200 Subject: [PATCH 244/265] Fix DeviceEntry.suggested_area deprecation warning (#149951) --- homeassistant/helpers/device_registry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 72d0cf651f2..c7f7d4c369d 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1221,8 +1221,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ("name", name), ("name_by_user", name_by_user), ("serial_number", serial_number), - # Can be removed when suggested_area is removed from DeviceEntry - ("suggested_area", suggested_area), ("sw_version", sw_version), ("via_device_id", via_device_id), ): @@ -1230,6 +1228,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values[attr_name] = value old_values[attr_name] = getattr(old, attr_name) + # Can be removed when suggested_area is removed from DeviceEntry + if suggested_area is not UNDEFINED and suggested_area != old._suggested_area: # noqa: SLF001 + new_values["suggested_area"] = suggested_area + old_values["suggested_area"] = old._suggested_area # noqa: SLF001 + if old.is_new: new_values["is_new"] = False From cbf4130bff17d746d72b8e307e195767fec40fbd Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Mon, 4 Aug 2025 12:26:22 +0200 Subject: [PATCH 245/265] Add zeroconf flow to Homee (#149820) Co-authored-by: Joostlek --- homeassistant/components/homee/config_flow.py | 157 ++++++++--- homeassistant/components/homee/const.py | 5 + homeassistant/components/homee/manifest.json | 8 +- homeassistant/components/homee/strings.json | 11 + homeassistant/generated/zeroconf.py | 4 + tests/components/homee/test_config_flow.py | 248 ++++++++++++++---- 6 files changed, 343 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/homee/config_flow.py b/homeassistant/components/homee/config_flow.py index 7030752f4c3..44c9b70953b 100644 --- a/homeassistant/components/homee/config_flow.py +++ b/homeassistant/components/homee/config_flow.py @@ -11,10 +11,16 @@ from pyHomee import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .const import DOMAIN +from .const import ( + DOMAIN, + RESULT_CANNOT_CONNECT, + RESULT_INVALID_AUTH, + RESULT_UNKNOWN_ERROR, +) _LOGGER = logging.getLogger(__name__) @@ -33,60 +39,137 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 homee: Homee + _host: str + _name: str _reauth_host: str _reauth_username: str + async def _connect_homee(self) -> dict[str, str]: + errors: dict[str, str] = {} + try: + await self.homee.get_access_token() + except HomeeConnectionFailedException: + errors["base"] = RESULT_CANNOT_CONNECT + except HomeeAuthenticationFailedException: + errors["base"] = RESULT_INVALID_AUTH + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = RESULT_UNKNOWN_ERROR + else: + _LOGGER.info("Got access token for homee") + self.hass.loop.create_task(self.homee.run()) + _LOGGER.debug("Homee task created") + await self.homee.wait_until_connected() + _LOGGER.info("Homee connected") + self.homee.disconnect() + _LOGGER.debug("Homee disconnecting") + await self.homee.wait_until_disconnected() + _LOGGER.info("Homee config successfully tested") + + await self.async_set_unique_id( + self.homee.settings.uid, raise_on_progress=self.source != SOURCE_USER + ) + + self._abort_if_unique_id_configured() + + _LOGGER.info("Created new homee entry with ID %s", self.homee.settings.uid) + + return errors + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial user step.""" + errors: dict[str, str] = {} - errors = {} if user_input is not None: self.homee = Homee( user_input[CONF_HOST], user_input[CONF_USERNAME], user_input[CONF_PASSWORD], ) + errors = await self._connect_homee() - try: - await self.homee.get_access_token() - except HomeeConnectionFailedException: - errors["base"] = "cannot_connect" - except HomeeAuthenticationFailedException: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - _LOGGER.info("Got access token for homee") - self.hass.loop.create_task(self.homee.run()) - _LOGGER.debug("Homee task created") - await self.homee.wait_until_connected() - _LOGGER.info("Homee connected") - self.homee.disconnect() - _LOGGER.debug("Homee disconnecting") - await self.homee.wait_until_disconnected() - _LOGGER.info("Homee config successfully tested") - - await self.async_set_unique_id(self.homee.settings.uid) - - self._abort_if_unique_id_configured() - - _LOGGER.info( - "Created new homee entry with ID %s", self.homee.settings.uid - ) - + if not errors: return self.async_create_entry( title=f"{self.homee.settings.homee_name} ({self.homee.host})", data=user_input, ) + return self.async_show_form( step_id="user", data_schema=AUTH_SCHEMA, errors=errors, ) + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle zeroconf discovery.""" + + # Ensure that an IPv4 address is received + self._host = discovery_info.host + self._name = discovery_info.hostname[6:18] + if discovery_info.ip_address.version == 6: + return self.async_abort(reason="ipv6_address") + + await self.async_set_unique_id(self._name) + self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) + + # Cause an auth-error to see if homee is reachable. + self.homee = Homee( + self._host, + "dummy_username", + "dummy_password", + ) + errors = await self._connect_homee() + if errors["base"] != RESULT_INVALID_AUTH: + return self.async_abort(reason=RESULT_CANNOT_CONNECT) + + self.context["title_placeholders"] = {"name": self._name, "host": self._host} + + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm the configuration of the device.""" + + errors: dict[str, str] = {} + if user_input is not None: + self.homee = Homee( + self._host, + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + + errors = await self._connect_homee() + + if not errors: + return self.async_create_entry( + title=f"{self.homee.settings.homee_name} ({self.homee.host})", + data={ + CONF_HOST: self._host, + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + + return self.async_show_form( + step_id="zeroconf_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + description_placeholders={ + CONF_HOST: self._name, + }, + last_step=True, + ) + async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: @@ -108,12 +191,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN): try: await self.homee.get_access_token() except HomeeConnectionFailedException: - errors["base"] = "cannot_connect" + errors["base"] = RESULT_CANNOT_CONNECT except HomeeAuthenticationFailedException: - errors["base"] = "invalid_auth" + errors["base"] = RESULT_INVALID_AUTH except Exception: _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + errors["base"] = RESULT_UNKNOWN_ERROR else: self.hass.loop.create_task(self.homee.run()) await self.homee.wait_until_connected() @@ -161,12 +244,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN): try: await self.homee.get_access_token() except HomeeConnectionFailedException: - errors["base"] = "cannot_connect" + errors["base"] = RESULT_CANNOT_CONNECT except HomeeAuthenticationFailedException: - errors["base"] = "invalid_auth" + errors["base"] = RESULT_INVALID_AUTH except Exception: _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + errors["base"] = RESULT_UNKNOWN_ERROR else: self.hass.loop.create_task(self.homee.run()) await self.homee.wait_until_connected() diff --git a/homeassistant/components/homee/const.py b/homeassistant/components/homee/const.py index 7bc3de189d6..718baf346ae 100644 --- a/homeassistant/components/homee/const.py +++ b/homeassistant/components/homee/const.py @@ -20,6 +20,11 @@ from homeassistant.const import ( # General DOMAIN = "homee" +# Error strings +RESULT_CANNOT_CONNECT = "cannot_connect" +RESULT_INVALID_AUTH = "invalid_auth" +RESULT_UNKNOWN_ERROR = "unknown" + # Sensor mappings HOMEE_UNIT_TO_HA_UNIT = { "": None, diff --git a/homeassistant/components/homee/manifest.json b/homeassistant/components/homee/manifest.json index 9cac876f325..35e89ec645a 100644 --- a/homeassistant/components/homee/manifest.json +++ b/homeassistant/components/homee/manifest.json @@ -8,5 +8,11 @@ "iot_class": "local_push", "loggers": ["homee"], "quality_scale": "silver", - "requirements": ["pyHomee==1.2.10"] + "requirements": ["pyHomee==1.2.10"], + "zeroconf": [ + { + "type": "_ssh._tcp.local.", + "name": "homee-*" + } + ] } diff --git a/homeassistant/components/homee/strings.json b/homeassistant/components/homee/strings.json index 267d5553a8c..26fa335d147 100644 --- a/homeassistant/components/homee/strings.json +++ b/homeassistant/components/homee/strings.json @@ -46,6 +46,17 @@ "data_description": { "host": "[%key:component::homee::config::step::user::data_description::host%]" } + }, + "zeroconf_confirm": { + "title": "Configure discovered homee {host}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "[%key:component::homee::config::step::user::data_description::username%]", + "password": "[%key:component::homee::config::step::user::data_description::password%]" + } } } }, diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index a3668acee8d..742840fa849 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -890,6 +890,10 @@ ZEROCONF = { }, ], "_ssh._tcp.local.": [ + { + "domain": "homee", + "name": "homee-*", + }, { "domain": "smappee", "name": "smappee1*", diff --git a/tests/components/homee/test_config_flow.py b/tests/components/homee/test_config_flow.py index 6f45dcbdb0d..3d2195443a2 100644 --- a/tests/components/homee/test_config_flow.py +++ b/tests/components/homee/test_config_flow.py @@ -1,15 +1,23 @@ """Test the Homee config flow.""" +from ipaddress import ip_address from unittest.mock import AsyncMock from pyHomee import HomeeAuthFailedException, HomeeConnectionFailedException import pytest -from homeassistant.components.homee.const import DOMAIN +from homeassistant import config_entries +from homeassistant.components.homee.const import ( + DOMAIN, + RESULT_CANNOT_CONNECT, + RESULT_INVALID_AUTH, + RESULT_UNKNOWN_ERROR, +) 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 homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .conftest import ( HOMEE_ID, @@ -24,6 +32,24 @@ from .conftest import ( from tests.common import MockConfigEntry +PARAMETRIZED_ERRORS = ( + ("side_eff", "error"), + [ + ( + HomeeConnectionFailedException("connection timed out"), + {"base": RESULT_CANNOT_CONNECT}, + ), + ( + HomeeAuthFailedException("wrong username or password"), + {"base": RESULT_INVALID_AUTH}, + ), + ( + Exception, + {"base": RESULT_UNKNOWN_ERROR}, + ), + ], +) + @pytest.mark.usefixtures("mock_homee", "mock_config_entry", "mock_setup_entry") async def test_config_flow( @@ -58,23 +84,7 @@ async def test_config_flow( assert result["result"].unique_id == HOMEE_ID -@pytest.mark.parametrize( - ("side_eff", "error"), - [ - ( - HomeeConnectionFailedException("connection timed out"), - {"base": "cannot_connect"}, - ), - ( - HomeeAuthFailedException("wrong username or password"), - {"base": "invalid_auth"}, - ), - ( - Exception, - {"base": "unknown"}, - ), - ], -) +@pytest.mark.parametrize(*PARAMETRIZED_ERRORS) async def test_config_flow_errors( hass: HomeAssistant, mock_homee: AsyncMock, @@ -140,6 +150,172 @@ async def test_flow_already_configured( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_homee", "mock_config_entry") +async def test_zeroconf_success( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_homee: AsyncMock, +) -> None: + """Test zeroconf discovery flow.""" + mock_homee.get_access_token.side_effect = HomeeAuthFailedException( + "wrong username or password" + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + name=f"homee-{HOMEE_ID}._ssh._tcp.local.", + type="_ssh._tcp.local.", + hostname=f"homee-{HOMEE_ID}.local.", + ip_address=ip_address(HOMEE_IP), + ip_addresses=[ip_address(HOMEE_IP)], + port=22, + properties={}, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "zeroconf_confirm" + assert result["handler"] == DOMAIN + mock_setup_entry.assert_not_called() + + mock_homee.get_access_token.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: TESTUSER, + CONF_PASSWORD: TESTPASS, + }, + ) + + assert result["data"] == { + CONF_HOST: HOMEE_IP, + CONF_USERNAME: TESTUSER, + CONF_PASSWORD: TESTPASS, + } + + mock_setup_entry.assert_called_once() + + +@pytest.mark.parametrize(*PARAMETRIZED_ERRORS) +async def test_zeroconf_confirm_errors( + hass: HomeAssistant, + mock_homee: AsyncMock, + side_eff: Exception, + error: dict[str, str], +) -> None: + """Test zeroconf discovery flow errors.""" + mock_homee.get_access_token.side_effect = HomeeAuthFailedException( + "wrong username or password" + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + name=f"homee-{HOMEE_ID}._ssh._tcp.local.", + type="_ssh._tcp.local.", + hostname=f"homee-{HOMEE_ID}.local.", + ip_address=ip_address(HOMEE_IP), + ip_addresses=[ip_address(HOMEE_IP)], + port=22, + properties={}, + ), + ) + + flow_id = result["flow_id"] + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "zeroconf_confirm" + assert result["handler"] == DOMAIN + + mock_homee.get_access_token.side_effect = side_eff + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: TESTUSER, + CONF_PASSWORD: TESTPASS, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == error + + mock_homee.get_access_token.side_effect = None + result = await hass.config_entries.flow.async_configure( + flow_id, + user_input={ + CONF_USERNAME: TESTUSER, + CONF_PASSWORD: TESTPASS, + }, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_zeroconf_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test zeroconf discovery flow when already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + name=f"homee-{HOMEE_ID}._ssh._tcp.local.", + type="_ssh._tcp.local.", + hostname=f"homee-{HOMEE_ID}.local.", + ip_address=ip_address(HOMEE_IP), + ip_addresses=[ip_address(HOMEE_IP)], + port=22, + properties={}, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("side_eff", "ip", "reason"), + [ + ( + HomeeConnectionFailedException("connection timed out"), + HOMEE_IP, + RESULT_CANNOT_CONNECT, + ), + (Exception, HOMEE_IP, RESULT_CANNOT_CONNECT), + (None, "2001:db8::1", "ipv6_address"), + ], +) +async def test_zeroconf_errors( + hass: HomeAssistant, + mock_homee: AsyncMock, + side_eff: Exception, + ip: str, + reason: str, +) -> None: + """Test zeroconf discovery flow with an IPv6 address.""" + mock_homee.get_access_token.side_effect = side_eff + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + name=f"homee-{HOMEE_ID}._ssh._tcp.local.", + type="_ssh._tcp.local.", + hostname=f"homee-{HOMEE_ID}.local.", + ip_address=ip_address(ip), + ip_addresses=[ip_address(ip)], + port=22, + properties={}, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + + @pytest.mark.usefixtures("mock_homee", "mock_setup_entry") async def test_reauth_success( hass: HomeAssistant, @@ -171,23 +347,7 @@ async def test_reauth_success( assert mock_config_entry.data[CONF_PASSWORD] == NEW_TESTPASS -@pytest.mark.parametrize( - ("side_eff", "error"), - [ - ( - HomeeConnectionFailedException("connection timed out"), - {"base": "cannot_connect"}, - ), - ( - HomeeAuthFailedException("wrong username or password"), - {"base": "invalid_auth"}, - ), - ( - Exception, - {"base": "unknown"}, - ), - ], -) +@pytest.mark.parametrize(*PARAMETRIZED_ERRORS) async def test_reauth_errors( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -296,23 +456,7 @@ async def test_reconfigure_success( assert mock_config_entry.data[CONF_PASSWORD] == TESTPASS -@pytest.mark.parametrize( - ("side_eff", "error"), - [ - ( - HomeeConnectionFailedException("connection timed out"), - {"base": "cannot_connect"}, - ), - ( - HomeeAuthFailedException("wrong username or password"), - {"base": "invalid_auth"}, - ), - ( - Exception, - {"base": "unknown"}, - ), - ], -) +@pytest.mark.parametrize(*PARAMETRIZED_ERRORS) async def test_reconfigure_errors( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From 106c086e8bc99eb6a504bd53421a484b2b5da761 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:29:27 +0200 Subject: [PATCH 246/265] Pass config entry to Unifi coordinator (#149952) --- homeassistant/components/unifi/hub/entity_loader.py | 8 +++++--- homeassistant/components/unifi/hub/hub.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/hub/entity_loader.py b/homeassistant/components/unifi/hub/entity_loader.py index 84948a92e98..4fd3d34a51d 100644 --- a/homeassistant/components/unifi/hub/entity_loader.py +++ b/homeassistant/components/unifi/hub/entity_loader.py @@ -25,6 +25,7 @@ from ..const import LOGGER, UNIFI_WIRELESS_CLIENTS from ..entity import UnifiEntity, UnifiEntityDescription if TYPE_CHECKING: + from .. import UnifiConfigEntry from .hub import UnifiHub CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) @@ -34,7 +35,7 @@ POLL_INTERVAL = timedelta(seconds=10) class UnifiEntityLoader: """UniFi Network integration handling platforms for entity registration.""" - def __init__(self, hub: UnifiHub) -> None: + def __init__(self, hub: UnifiHub, config_entry: UnifiConfigEntry) -> None: """Initialize the UniFi entity loader.""" self.hub = hub self.api_updaters = ( @@ -57,15 +58,16 @@ class UnifiEntityLoader: ) self.wireless_clients = hub.hass.data[UNIFI_WIRELESS_CLIENTS] - self._dataUpdateCoordinator = DataUpdateCoordinator( + self._data_update_coordinator = DataUpdateCoordinator( hub.hass, LOGGER, name="Unifi entity poller", + config_entry=config_entry, update_method=self._update_pollable_api_data, update_interval=POLL_INTERVAL, ) - self._update_listener = self._dataUpdateCoordinator.async_add_listener( + self._update_listener = self._data_update_coordinator.async_add_listener( update_callback=lambda: None ) diff --git a/homeassistant/components/unifi/hub/hub.py b/homeassistant/components/unifi/hub/hub.py index 6cf8825a26c..9ea887bdb29 100644 --- a/homeassistant/components/unifi/hub/hub.py +++ b/homeassistant/components/unifi/hub/hub.py @@ -39,7 +39,7 @@ class UnifiHub: self.hass = hass self.api = api self.config = UnifiConfig.from_config_entry(config_entry) - self.entity_loader = UnifiEntityLoader(self) + self.entity_loader = UnifiEntityLoader(self, config_entry) self._entity_helper = UnifiEntityHelper(hass, api) self.websocket = UnifiWebsocket(hass, api, self.signal_reachable) From c2b298283e5f8b30198c775a70b9e4ee0e3e8108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 4 Aug 2025 11:32:01 +0100 Subject: [PATCH 247/265] Bump hass-nabucasa from 0.110.0 to 0.110.1 (#149956) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index a819203e549..63eae6261d4 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.110.0"], + "requirements": ["hass-nabucasa==0.110.1"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 80354c706d6..f8a57ba61bb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==4.0.1 -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250731.0 diff --git a/pyproject.toml b/pyproject.toml index 2bcb7787601..a32e9308fe2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.110.0", + "hass-nabucasa==0.110.1", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", diff --git a/requirements.txt b/requirements.txt index a332eb930c2..ba08a72e324 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 52b2fdb42b8..8dcc7478d7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ habiticalib==0.4.1 habluetooth==4.0.1 # homeassistant.components.cloud -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffed56c6f3e..ce01dbd5a51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ habiticalib==0.4.1 habluetooth==4.0.1 # homeassistant.components.cloud -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 # homeassistant.components.assist_satellite # homeassistant.components.conversation From 93e11aa8bc7dcda4e1b9ae467540908b719b2d6b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:35:24 +0200 Subject: [PATCH 248/265] Refresh plugwise test-fixtures (#149875) --- tests/components/plugwise/fixtures/legacy_anna/data.json | 2 ++ .../fixtures/m_adam_multiple_devices_per_zone/data.json | 3 ++- tests/components/plugwise/snapshots/test_diagnostics.ambr | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/components/plugwise/fixtures/legacy_anna/data.json b/tests/components/plugwise/fixtures/legacy_anna/data.json index cc7e66fb174..75c12a4c8c2 100644 --- a/tests/components/plugwise/fixtures/legacy_anna/data.json +++ b/tests/components/plugwise/fixtures/legacy_anna/data.json @@ -35,6 +35,7 @@ }, "0d266432d64443e283b5d708ae98b455": { "active_preset": "home", + "available_schedules": [], "climate_mode": "heat", "control_state": "heating", "dev_class": "thermostat", @@ -44,6 +45,7 @@ "model": "ThermoTouch", "name": "Anna", "preset_modes": ["away", "vacation", "asleep", "home", "no_frost"], + "select_schedule": null, "sensors": { "illuminance": 150.8, "setpoint": 20.5, diff --git a/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json b/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json index 06459a11798..f1880ba69e1 100644 --- a/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json +++ b/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json @@ -112,12 +112,14 @@ }, "446ac08dd04d4eff8ac57489757b7314": { "active_preset": "no_frost", + "available_schedules": [], "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "model": "ThermoZone", "name": "Garage", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": null, "sensors": { "temperature": 15.6 }, @@ -587,7 +589,6 @@ "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." } }, - "select_regulation_mode": "heating", "sensors": { "outdoor_temperature": 7.81 }, diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 4aa367bc116..91411c323ac 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -131,6 +131,8 @@ }), '446ac08dd04d4eff8ac57489757b7314': dict({ 'active_preset': 'no_frost', + 'available_schedules': list([ + ]), 'climate_mode': 'heat', 'control_state': 'idle', 'dev_class': 'climate', @@ -143,6 +145,7 @@ 'vacation', 'no_frost', ]), + 'select_schedule': None, 'sensors': dict({ 'temperature': 15.6, }), @@ -635,7 +638,6 @@ 'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", }), }), - 'select_regulation_mode': 'heating', 'sensors': dict({ 'outdoor_temperature': 7.81, }), From 9edd24273457234ff35c84a4b78b89b48afeda05 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:49:26 +0200 Subject: [PATCH 249/265] Pass config entry to SMS coordinator (#149955) --- homeassistant/components/sms/__init__.py | 4 ++-- homeassistant/components/sms/coordinator.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 6c7c5374f7d..78f7899a571 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -83,8 +83,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not gateway: raise ConfigEntryNotReady(f"Cannot find device {device}") - signal_coordinator = SignalCoordinator(hass, gateway) - network_coordinator = NetworkCoordinator(hass, gateway) + signal_coordinator = SignalCoordinator(hass, entry, gateway) + network_coordinator = NetworkCoordinator(hass, entry, gateway) # Fetch initial data so we have data when entities subscribe await signal_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/sms/coordinator.py b/homeassistant/components/sms/coordinator.py index 7bc691afedf..858fc303805 100644 --- a/homeassistant/components/sms/coordinator.py +++ b/homeassistant/components/sms/coordinator.py @@ -16,13 +16,14 @@ _LOGGER = logging.getLogger(__name__) class SignalCoordinator(DataUpdateCoordinator): """Signal strength coordinator.""" - def __init__(self, hass, gateway): + def __init__(self, hass, entry, gateway): """Initialize signal strength coordinator.""" super().__init__( hass, _LOGGER, name="Device signal state", update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + config_entry=entry, ) self._gateway = gateway @@ -38,13 +39,14 @@ class SignalCoordinator(DataUpdateCoordinator): class NetworkCoordinator(DataUpdateCoordinator): """Network info coordinator.""" - def __init__(self, hass, gateway): + def __init__(self, hass, entry, gateway): """Initialize network info coordinator.""" super().__init__( hass, _LOGGER, name="Device network state", update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + config_entry=entry, ) self._gateway = gateway From 9f867f268ce0072e48248e0a1fc8aad2d95e1f0f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:58:19 +0200 Subject: [PATCH 250/265] Pass config entry to Snoo coordinator (#149947) --- homeassistant/components/snoo/__init__.py | 2 +- homeassistant/components/snoo/coordinator.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snoo/__init__.py b/homeassistant/components/snoo/__init__.py index 54834bf58ce..20d94be7c03 100644 --- a/homeassistant/components/snoo/__init__.py +++ b/homeassistant/components/snoo/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SnooConfigEntry) -> bool coordinators: dict[str, SnooCoordinator] = {} tasks = [] for device in devices: - coordinators[device.serialNumber] = SnooCoordinator(hass, device, snoo) + coordinators[device.serialNumber] = SnooCoordinator(hass, entry, device, snoo) tasks.append(coordinators[device.serialNumber].setup()) await asyncio.gather(*tasks) entry.runtime_data = coordinators diff --git a/homeassistant/components/snoo/coordinator.py b/homeassistant/components/snoo/coordinator.py index bc06d20955c..8ce0db34621 100644 --- a/homeassistant/components/snoo/coordinator.py +++ b/homeassistant/components/snoo/coordinator.py @@ -19,11 +19,18 @@ class SnooCoordinator(DataUpdateCoordinator[SnooData]): config_entry: SnooConfigEntry - def __init__(self, hass: HomeAssistant, device: SnooDevice, snoo: Snoo) -> None: + def __init__( + self, + hass: HomeAssistant, + entry: SnooConfigEntry, + device: SnooDevice, + snoo: Snoo, + ) -> None: """Set up Snoo Coordinator.""" super().__init__( hass, name=device.name, + config_entry=entry, logger=_LOGGER, ) self.device_unique_id = device.serialNumber From 594ce8f266902be504d7bd01328a4b070f1cd441 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 12:58:46 +0200 Subject: [PATCH 251/265] Pass config entry to Smarttub coordinator (#149946) --- homeassistant/components/smarttub/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 337959e0316..095179d618a 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -74,6 +74,7 @@ class SmartTubController: self._hass, _LOGGER, name=DOMAIN, + config_entry=entry, update_method=self.async_update_data, update_interval=timedelta(seconds=SCAN_INTERVAL), ) From a962777a2e04bf2adb17a7d80b449edee87f7a76 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:14:50 +0200 Subject: [PATCH 252/265] Pass config entry to Meteo France coordinator (#149945) --- homeassistant/components/meteo_france/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 20e6c02f5d4..94918ab4d4f 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -63,6 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, name=f"Météo-France forecast for city {entry.title}", + config_entry=entry, update_method=_async_update_data_forecast_forecast, update_interval=SCAN_INTERVAL, ) @@ -80,6 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, name=f"Météo-France rain for city {entry.title}", + config_entry=entry, update_method=_async_update_data_rain, update_interval=SCAN_INTERVAL_RAIN, ) @@ -103,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, name=f"Météo-France alert for department {department}", + config_entry=entry, update_method=_async_update_data_alert, update_interval=SCAN_INTERVAL, ) From 39b651e07560fac1de222a34af40989fd447b736 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:17:27 +0200 Subject: [PATCH 253/265] Pass config entry to Kraken coordinator (#149944) --- homeassistant/components/kraken/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index c981f3fd438..5c3158bddf2 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -135,6 +135,7 @@ class KrakenData: self._hass, _LOGGER, name=DOMAIN, + config_entry=self._config_entry, update_method=self.async_update, update_interval=timedelta( seconds=self._config_entry.options[CONF_SCAN_INTERVAL] From 3d27d501b17192728a91f7746bfe0f2448c28ea4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:20:30 +0200 Subject: [PATCH 254/265] Pass config entry to Mill coordinator (#149942) --- homeassistant/components/mill/__init__.py | 1 + homeassistant/components/mill/coordinator.py | 2 ++ tests/components/mill/test_coordinator.py | 19 ++++++++++++++----- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 246ea778916..ce258712090 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -43,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: historic_data_coordinator = MillHistoricDataUpdateCoordinator( hass, + entry, mill_data_connection=mill_data_connection, ) historic_data_coordinator.async_add_listener(lambda: None) diff --git a/homeassistant/components/mill/coordinator.py b/homeassistant/components/mill/coordinator.py index a701acb8ddb..ea1295376ae 100644 --- a/homeassistant/components/mill/coordinator.py +++ b/homeassistant/components/mill/coordinator.py @@ -60,6 +60,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, *, mill_data_connection: Mill, ) -> None: @@ -70,6 +71,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name="MillHistoricDataUpdateCoordinator", + config_entry=config_entry, ) async def _async_update_data(self): diff --git a/tests/components/mill/test_coordinator.py b/tests/components/mill/test_coordinator.py index a2a3bd57b65..2e6e08016b7 100644 --- a/tests/components/mill/test_coordinator.py +++ b/tests/components/mill/test_coordinator.py @@ -11,12 +11,15 @@ from homeassistant.components.recorder.statistics import statistics_during_perio from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry from tests.components.recorder.common import async_wait_recording_done async def test_mill_historic_data(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -31,7 +34,7 @@ async def test_mill_historic_data(recorder_mock: Recorder, hass: HomeAssistant) statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -96,6 +99,8 @@ async def test_mill_historic_data_no_heater( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -110,7 +115,7 @@ async def test_mill_historic_data_no_heater( statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -133,6 +138,8 @@ async def test_mill_historic_data_no_data( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -145,7 +152,7 @@ async def test_mill_historic_data_no_data( mill_data_connection.fetch_historic_energy_usage = AsyncMock(return_value=data) coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -168,7 +175,7 @@ async def test_mill_historic_data_no_data( mill_data_connection.fetch_historic_energy_usage = AsyncMock(return_value=None) coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -192,6 +199,8 @@ async def test_mill_historic_data_invalid_data( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): None, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -206,7 +215,7 @@ async def test_mill_historic_data_invalid_data( statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) From 33eaca24d6d4cda2c7198b48e875a99be46f94ed Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:21:29 +0200 Subject: [PATCH 255/265] Pass config entry to Simplisafe coordinator (#149943) --- homeassistant/components/simplisafe/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 8a75baa69c6..67bf94c61ae 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -573,6 +573,7 @@ class SimpliSafe: self._hass, LOGGER, name=self.entry.title, + config_entry=self.entry, update_interval=DEFAULT_SCAN_INTERVAL, update_method=self.async_update, ) From 7a6aaf667bf3b80b1e53299ab6f719bef97a5779 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:27:10 +0200 Subject: [PATCH 256/265] Pass config entry to hue coordinator (#149941) --- homeassistant/components/hue/v1/light.py | 2 ++ homeassistant/components/hue/v1/sensor_base.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index b7251382296..36dfdd423ef 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -163,6 +163,7 @@ async def async_setup_entry( name="light", update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update), update_interval=SCAN_INTERVAL, + config_entry=config_entry, request_refresh_debouncer=Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), @@ -197,6 +198,7 @@ async def async_setup_entry( name="group", update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update), update_interval=SCAN_INTERVAL, + config_entry=config_entry, request_refresh_debouncer=Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index 393069b0c7c..fb8f3c572c1 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -53,6 +53,7 @@ class SensorManager: LOGGER, name="sensor", update_method=self.async_update_data, + config_entry=bridge.config_entry, update_interval=self.SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True From 312e5903609e8292c7b25eee0a0f9f24e581d06e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:27:51 +0200 Subject: [PATCH 257/265] Pass config entry to Broadlink coordinator (#149949) --- homeassistant/components/broadlink/updater.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 7c1644fff54..8fdbb5054a8 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -64,6 +64,7 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]): device.hass, _LOGGER, name=f"{device.name} ({device.api.model} at {device.api.host[0]})", + config_entry=device.config, update_method=self.async_update, update_interval=self.SCAN_INTERVAL, ) From 0bdf6757c49a2544c22c344ba347d62b8d0c140f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Aug 2025 13:28:59 +0200 Subject: [PATCH 258/265] Pass config entry to Remote Calendar coordinator (#149958) --- homeassistant/components/remote_calendar/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/remote_calendar/coordinator.py b/homeassistant/components/remote_calendar/coordinator.py index 26876b53224..7a7abe37b89 100644 --- a/homeassistant/components/remote_calendar/coordinator.py +++ b/homeassistant/components/remote_calendar/coordinator.py @@ -39,6 +39,7 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]): _LOGGER, name=f"{DOMAIN}_{config_entry.title}", update_interval=SCAN_INTERVAL, + config_entry=config_entry, always_update=True, ) self._client = get_async_client(hass) From 46cfdddc80e505c25955858c7c3a77e39132a38d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 4 Aug 2025 12:29:11 +0100 Subject: [PATCH 259/265] Move to the new handler for migrate_paypal_agreement (#149934) --- homeassistant/components/cloud/subscription.py | 16 +++++++++------- tests/components/cloud/conftest.py | 1 + tests/components/cloud/test_repairs.py | 9 ++++++++- tests/components/cloud/test_subscription.py | 6 ++---- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cloud/subscription.py b/homeassistant/components/cloud/subscription.py index 9ee154dbff4..c1b8fc095c3 100644 --- a/homeassistant/components/cloud/subscription.py +++ b/homeassistant/components/cloud/subscription.py @@ -4,11 +4,13 @@ from __future__ import annotations import asyncio import logging -from typing import Any -from aiohttp.client_exceptions import ClientError -from hass_nabucasa import Cloud, cloud_api -from hass_nabucasa.payments_api import PaymentsApiError, SubscriptionInfo +from hass_nabucasa import ( + Cloud, + MigratePaypalAgreementInfo, + PaymentsApiError, + SubscriptionInfo, +) from .client import CloudClient from .const import REQUEST_TIMEOUT @@ -29,17 +31,17 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo async def async_migrate_paypal_agreement( cloud: Cloud[CloudClient], -) -> dict[str, Any] | None: +) -> MigratePaypalAgreementInfo | None: """Migrate a paypal agreement from legacy.""" try: async with asyncio.timeout(REQUEST_TIMEOUT): - return await cloud_api.async_migrate_paypal_agreement(cloud) + return await cloud.payments.migrate_paypal_agreement() except TimeoutError: _LOGGER.error( "A timeout of %s was reached while trying to start agreement migration", REQUEST_TIMEOUT, ) - except ClientError as exception: + except PaymentsApiError as exception: _LOGGER.error("Failed to start agreement migration - %s", exception) return None diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index e63af0ced09..a4625fcce92 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -74,6 +74,7 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock]: mock_cloud.payments = MagicMock( spec=payments_api.PaymentsApi, subscription_info=AsyncMock(), + migrate_paypal_agreement=AsyncMock(), ) mock_cloud.ice_servers = MagicMock( spec=IceServers, diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index bb3c874c077..0377ee81dba 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -4,6 +4,7 @@ from datetime import timedelta from http import HTTPStatus from unittest.mock import patch +from hass_nabucasa.payments_api import PaymentsApiError import pytest from homeassistant.components.cloud.const import DOMAIN @@ -210,7 +211,13 @@ async def test_legacy_subscription_repair_flow_timeout( "preview": None, } - with patch("homeassistant.components.cloud.repairs.MAX_RETRIES", new=0): + with ( + patch("homeassistant.components.cloud.repairs.MAX_RETRIES", new=0), + patch( + "hass_nabucasa.payments_api.PaymentsApi.migrate_paypal_agreement", + side_effect=PaymentsApiError("some error", status=403), + ), + ): resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") assert resp.status == HTTPStatus.OK data = await resp.json() diff --git a/tests/components/cloud/test_subscription.py b/tests/components/cloud/test_subscription.py index c34ca1bc871..ba45e6bca57 100644 --- a/tests/components/cloud/test_subscription.py +++ b/tests/components/cloud/test_subscription.py @@ -25,6 +25,7 @@ async def mocked_cloud_object(hass: HomeAssistant) -> Cloud: payments=Mock( spec=payments_api.PaymentsApi, subscription_info=AsyncMock(), + migrate_paypal_agreement=AsyncMock(), ), ) @@ -52,10 +53,7 @@ async def test_migrate_paypal_agreement_with_timeout_error( mocked_cloud: Cloud, ) -> None: """Test that we handle timeout error.""" - aioclient_mock.post( - "https://accounts.nabucasa.com/payments/migrate_paypal_agreement", - exc=TimeoutError(), - ) + mocked_cloud.payments.migrate_paypal_agreement.side_effect = TimeoutError() assert await async_migrate_paypal_agreement(mocked_cloud) is None assert ( From e2bc73f153b9a00658cd9e2652a731b86c2198a3 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 4 Aug 2025 07:35:13 -0400 Subject: [PATCH 260/265] Fix optimistic covers (#149962) --- homeassistant/components/template/cover.py | 1 + homeassistant/components/template/entity.py | 12 +++++++++--- tests/components/template/test_cover.py | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index caac8cf5a1d..44981fcb08f 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -216,6 +216,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): _entity_id_format = ENTITY_ID_FORMAT _optimistic_entity = True + _extra_optimistic_options = (CONF_POSITION,) # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. # This ensures that the __init__ on AbstractTemplateEntity is not called twice. diff --git a/homeassistant/components/template/entity.py b/homeassistant/components/template/entity.py index e9a630594d7..03a93f50ec3 100644 --- a/homeassistant/components/template/entity.py +++ b/homeassistant/components/template/entity.py @@ -20,6 +20,7 @@ class AbstractTemplateEntity(Entity): _entity_id_format: str _optimistic_entity: bool = False + _extra_optimistic_options: tuple[str, ...] | None = None _template: Template | None = None def __init__( @@ -35,9 +36,14 @@ class AbstractTemplateEntity(Entity): if self._optimistic_entity: self._template = config.get(CONF_STATE) - self._attr_assumed_state = self._template is None or config.get( - CONF_OPTIMISTIC, False - ) + optimistic = self._template is None + if self._extra_optimistic_options: + optimistic = optimistic and all( + config.get(option) is None + for option in self._extra_optimistic_options + ) + + self._attr_assumed_state = optimistic or config.get(CONF_OPTIMISTIC, False) if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index dc3428330b0..692567c7aa8 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -239,6 +239,7 @@ async def setup_position_cover( { TEST_OBJECT_ID: { **COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position_template": position_template, } }, @@ -249,6 +250,7 @@ async def setup_position_cover( count, { **NAMED_COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position": position_template, }, ) @@ -258,6 +260,7 @@ async def setup_position_cover( count, { **NAMED_COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position": position_template, }, ) @@ -565,6 +568,7 @@ async def test_template_position( position: int | None, expected: str, caplog: pytest.LogCaptureFixture, + calls: list[ServiceCall], ) -> None: """Test the position_template attribute.""" hass.states.async_set(TEST_STATE_ENTITY_ID, CoverState.OPEN) @@ -580,6 +584,19 @@ async def test_template_position( assert state.state == expected assert "ValueError" not in caplog.text + # Test to make sure optimistic is not set with only a position template. + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, "position": 10}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("current_position") == position + assert state.state == expected + @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( From 1632e0aef69b2e6962af5084b6ea00ea59b9366b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 4 Aug 2025 13:36:12 +0200 Subject: [PATCH 261/265] Direct migrations with Z-Wave JS UI to docs (#149966) --- .../components/zwave_js/config_flow.py | 18 ++++++++++++++++-- homeassistant/components/zwave_js/strings.json | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 308e6c9cc1a..6121bd00508 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -93,6 +93,10 @@ MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61") NETWORK_TYPE_NEW = "new" NETWORK_TYPE_EXISTING = "existing" +ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = ( + "https://www.home-assistant.io/integrations/zwave_js/" + "#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui" +) def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: @@ -446,7 +450,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): None, ) if not self._reconfigure_config_entry: - return self.async_abort(reason="addon_required") + return self.async_abort( + reason="addon_required", + description_placeholders={ + "zwave_js_ui_migration": ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS, + }, + ) vid = discovery_info.vid pid = discovery_info.pid @@ -890,7 +899,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): config_entry = self._reconfigure_config_entry assert config_entry is not None if not self._usb_discovery and not config_entry.data.get(CONF_USE_ADDON): - return self.async_abort(reason="addon_required") + return self.async_abort( + reason="addon_required", + description_placeholders={ + "zwave_js_ui_migration": ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS, + }, + ) try: driver = self._get_driver() diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 0288fbd7131..8ac356a40b0 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -4,7 +4,7 @@ "addon_get_discovery_info_failed": "Failed to get Z-Wave add-on discovery info.", "addon_info_failed": "Failed to get Z-Wave add-on info.", "addon_install_failed": "Failed to install the Z-Wave add-on.", - "addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor add-on. You can still use the Backup and Restore buttons to migrate your network manually.", + "addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor add-on. If you are using Z-Wave JS UI, please follow our [migration instructions]({zwave_js_ui_migration}).", "addon_set_config_failed": "Failed to set Z-Wave configuration.", "addon_start_failed": "Failed to start the Z-Wave add-on.", "addon_stop_failed": "Failed to stop the Z-Wave add-on.", From 822e1ffc8d8b63d3712e6a85905ba99a764ee403 Mon Sep 17 00:00:00 2001 From: hanwg Date: Mon, 4 Aug 2025 20:27:15 +0800 Subject: [PATCH 262/265] Minor UI improvements for Telegram bot actions (#149889) --- .../components/telegram_bot/services.yaml | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index ce7ebea2b66..0ebe7988642 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -18,7 +18,8 @@ send_message: target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -43,7 +44,8 @@ send_message: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or ["Text button1:/button1, Text @@ -98,10 +100,12 @@ send_photo: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -126,7 +130,8 @@ send_photo: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -180,10 +185,12 @@ send_sticker: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true disable_notification: selector: boolean: @@ -199,7 +206,8 @@ send_sticker: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -253,10 +261,12 @@ send_animation: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -281,7 +291,8 @@ send_animation: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -335,10 +346,12 @@ send_video: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -363,7 +376,8 @@ send_video: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -417,10 +431,12 @@ send_voice: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true disable_notification: selector: boolean: @@ -436,7 +452,8 @@ send_voice: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -490,10 +507,12 @@ send_document: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -518,7 +537,8 @@ send_document: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -563,7 +583,8 @@ send_location: target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true disable_notification: selector: boolean: @@ -576,7 +597,8 @@ send_location: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -605,7 +627,8 @@ send_poll: target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true question: required: true selector: From b76f47cd9ff3d14c84be44ac6cce74a5776f3675 Mon Sep 17 00:00:00 2001 From: hanwg Date: Mon, 4 Aug 2025 20:32:48 +0800 Subject: [PATCH 263/265] Add bot details to Telegram bot events (#148638) --- homeassistant/components/telegram_bot/bot.py | 20 +++++++++++++++++- .../components/telegram_bot/polling.py | 2 +- .../components/telegram_bot/webhooks.py | 2 +- tests/components/telegram_bot/conftest.py | 2 +- .../telegram_bot/test_telegram_bot.py | 21 ++++++++++++++++++- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/telegram_bot/bot.py b/homeassistant/components/telegram_bot/bot.py index c57648c9551..3145badbed7 100644 --- a/homeassistant/components/telegram_bot/bot.py +++ b/homeassistant/components/telegram_bot/bot.py @@ -101,13 +101,26 @@ _LOGGER = logging.getLogger(__name__) type TelegramBotConfigEntry = ConfigEntry[TelegramNotificationService] +def _get_bot_info(bot: Bot, config_entry: ConfigEntry) -> dict[str, Any]: + return { + "config_entry_id": config_entry.entry_id, + "id": bot.id, + "first_name": bot.first_name, + "last_name": bot.last_name, + "username": bot.username, + } + + class BaseTelegramBot: """The base class for the telegram bot.""" - def __init__(self, hass: HomeAssistant, config: TelegramBotConfigEntry) -> None: + def __init__( + self, hass: HomeAssistant, config: TelegramBotConfigEntry, bot: Bot + ) -> None: """Initialize the bot base class.""" self.hass = hass self.config = config + self._bot = bot @abstractmethod async def shutdown(self) -> None: @@ -134,6 +147,8 @@ class BaseTelegramBot: _LOGGER.warning("Unhandled update: %s", update) return True + event_data["bot"] = _get_bot_info(self._bot, self.config) + event_context = Context() _LOGGER.debug("Firing event %s: %s", event_type, event_data) @@ -442,6 +457,9 @@ class TelegramNotificationService: event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[ ATTR_MESSAGE_THREAD_ID ] + + event_data["bot"] = _get_bot_info(self.bot, self.config) + self.hass.bus.async_fire( EVENT_TELEGRAM_SENT, event_data, context=context ) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 6c38a0e53b8..b8640c5c005 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -54,7 +54,7 @@ class PollBot(BaseTelegramBot): self, hass: HomeAssistant, bot: Bot, config: TelegramBotConfigEntry ) -> None: """Create Application to poll for updates.""" - super().__init__(hass, config) + super().__init__(hass, config, bot) self.bot = bot self.application = ApplicationBuilder().bot(self.bot).build() self.application.add_handler(TypeHandler(Update, self.handle_update)) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 29c3305858b..61843e6ffbf 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -77,7 +77,7 @@ class PushBot(BaseTelegramBot): # Dumb Application that just gets our updates to our handler callback (self.handle_update) self.application = ApplicationBuilder().bot(bot).updater(None).build() self.application.add_handler(TypeHandler(Update, self.handle_update)) - super().__init__(hass, config) + super().__init__(hass, config, bot) self.base_url = config.data.get(CONF_URL) or get_url( hass, require_ssl=True, allow_internal=False diff --git a/tests/components/telegram_bot/conftest.py b/tests/components/telegram_bot/conftest.py index 66c3c43ea86..489cb034ac2 100644 --- a/tests/components/telegram_bot/conftest.py +++ b/tests/components/telegram_bot/conftest.py @@ -96,7 +96,7 @@ def mock_external_calls() -> Generator[None]: max_reaction_count=100, accent_color_id=AccentColor.COLOR_000, ) - test_user = User(123456, "Testbot", True) + test_user = User(123456, "Testbot", True, "mock last name", "mock username") message = Message( message_id=12345, date=datetime.now(), diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index 80b9859ceab..eec2bd5ecf7 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -174,6 +174,15 @@ async def test_send_message( assert len(events) == 1 assert events[0].context == context + config_entry = hass.config_entries.async_entry_for_domain_unique_id( + DOMAIN, "1234567890:ABC" + ) + assert events[0].data["bot"]["config_entry_id"] == config_entry.entry_id + assert events[0].data["bot"]["id"] == 123456 + assert events[0].data["bot"]["first_name"] == "Testbot" + assert events[0].data["bot"]["last_name"] == "mock last name" + assert events[0].data["bot"]["username"] == "mock username" + assert len(response["chats"]) == 1 assert (response["chats"][0]["message_id"]) == 12345 @@ -479,6 +488,16 @@ async def test_polling_platform_message_text_update( assert len(events) == 1 assert events[0].data["text"] == update_message_text["message"]["text"] + + config_entry = hass.config_entries.async_entry_for_domain_unique_id( + DOMAIN, "1234567890:ABC" + ) + assert events[0].data["bot"]["config_entry_id"] == config_entry.entry_id + assert events[0].data["bot"]["id"] == 123456 + assert events[0].data["bot"]["first_name"] == "Testbot" + assert events[0].data["bot"]["last_name"] == "mock last name" + assert events[0].data["bot"]["username"] == "mock username" + assert isinstance(events[0].context, Context) @@ -752,7 +771,7 @@ async def test_send_message_no_chat_id_error( ) assert err.value.translation_key == "missing_allowed_chat_ids" - assert err.value.translation_placeholders["bot_name"] == "Testbot" + assert err.value.translation_placeholders["bot_name"] == "Testbot mock last name" async def test_send_message_config_entry_error( From 88c9d5dbe3804474f1fb727614a21af44a7df35f Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Mon, 4 Aug 2025 15:35:41 +0200 Subject: [PATCH 264/265] Fix bsblan reauthentication (#149926) --- .../components/bsblan/config_flow.py | 32 ++-- tests/components/bsblan/test_config_flow.py | 158 ++++++++++++++++++ 2 files changed, 170 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index 1491322ae13..5f4f67a114a 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -211,16 +211,16 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): ), ) - # Use existing host and port, update auth credentials - self.host = existing_entry.data[CONF_HOST] - self.port = existing_entry.data[CONF_PORT] - self.passkey = user_input.get(CONF_PASSKEY) or existing_entry.data.get( - CONF_PASSKEY - ) - self.username = user_input.get(CONF_USERNAME) or existing_entry.data.get( - CONF_USERNAME - ) - self.password = user_input.get(CONF_PASSWORD) + # Combine existing data with the user's new input for validation. + # This correctly handles adding, changing, and clearing credentials. + config_data = existing_entry.data.copy() + config_data.update(user_input) + + self.host = config_data[CONF_HOST] + self.port = config_data[CONF_PORT] + self.passkey = config_data.get(CONF_PASSKEY) + self.username = config_data.get(CONF_USERNAME) + self.password = config_data.get(CONF_PASSWORD) try: await self._get_bsblan_info(raise_on_progress=False, is_reauth=True) @@ -267,17 +267,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): errors={"base": "cannot_connect"}, ) - # Update the config entry with new auth data - data_updates = {} - if self.passkey is not None: - data_updates[CONF_PASSKEY] = self.passkey - if self.username is not None: - data_updates[CONF_USERNAME] = self.username - if self.password is not None: - data_updates[CONF_PASSWORD] = self.password - + # Update only the fields that were provided by the user return self.async_update_reload_and_abort( - existing_entry, data_updates=data_updates, reason="reauth_successful" + existing_entry, data_updates=user_input, reason="reauth_successful" ) @callback diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 3ca0de5b78f..a06131f7216 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -866,6 +866,164 @@ async def test_reauth_flow_partial_credentials_update( assert mock_config_entry.data[CONF_PORT] == 80 +async def test_reauth_flow_preserves_non_credential_fields( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test reauth flow preserves non-credential fields using data_updates.""" + # Create a config entry with additional custom fields that should be preserved + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + CONF_PASSKEY: "old_key", + CONF_USERNAME: "old_user", + CONF_PASSWORD: "old_pass", + # Add some custom fields that should be preserved + "custom_field": "should_be_preserved", + "another_field": 42, + }, + unique_id="00:80:41:19:69:90", + ) + entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + ) + + # Submit with only new credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "new_key", + CONF_USERNAME: "new_user", + CONF_PASSWORD: "new_pass", + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify that only the provided fields were updated, others preserved + assert entry.data[CONF_PASSKEY] == "new_key" # Updated + assert entry.data[CONF_USERNAME] == "new_user" # Updated + assert entry.data[CONF_PASSWORD] == "new_pass" # Updated + + # These fields should remain unchanged (preserved by data_updates) + assert entry.data[CONF_HOST] == "127.0.0.1" + assert entry.data[CONF_PORT] == 80 + assert entry.data["custom_field"] == "should_be_preserved" + assert entry.data["another_field"] == 42 + + +async def test_reauth_flow_clears_credentials_with_empty_strings( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test reauth flow can clear credentials by providing empty strings.""" + # Create a config entry with existing credentials + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + CONF_PASSKEY: "existing_key", + CONF_USERNAME: "existing_user", + CONF_PASSWORD: "existing_pass", + }, + unique_id="00:80:41:19:69:90", + ) + entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + ) + + # Submit with empty strings to clear credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "", # Clear passkey + CONF_USERNAME: "", # Clear username + CONF_PASSWORD: "", # Clear password + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify that credentials were cleared (set to empty strings) + assert entry.data[CONF_PASSKEY] == "" + assert entry.data[CONF_USERNAME] == "" + assert entry.data[CONF_PASSWORD] == "" + + # Host and port should remain unchanged + assert entry.data[CONF_HOST] == "127.0.0.1" + assert entry.data[CONF_PORT] == 80 + + +async def test_reauth_flow_partial_clear_credentials( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test reauth flow can partially clear some credentials while updating others.""" + # Create a config entry with existing credentials + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + CONF_PASSKEY: "existing_key", + CONF_USERNAME: "existing_user", + CONF_PASSWORD: "existing_pass", + }, + unique_id="00:80:41:19:69:90", + ) + entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + ) + + # Submit with mix of clearing and updating credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "", # Clear passkey + CONF_USERNAME: "new_user", # Update username + CONF_PASSWORD: "", # Clear password + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify mixed update: some cleared, some updated, some preserved + assert entry.data[CONF_PASSKEY] == "" # Cleared + assert entry.data[CONF_USERNAME] == "new_user" # Updated + assert entry.data[CONF_PASSWORD] == "" # Cleared + + # Host and port should remain unchanged + assert entry.data[CONF_HOST] == "127.0.0.1" + assert entry.data[CONF_PORT] == 80 + + async def test_zeroconf_discovery_auth_error_during_confirm( hass: HomeAssistant, mock_bsblan: MagicMock, From ae48179e959c8cc42dce3bb31d32136d34800e1e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 4 Aug 2025 15:58:57 +0200 Subject: [PATCH 265/265] Bump zwave-js-server-python to 0.67.1 (#149972) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 2cad8df3805..153e8e6a7fe 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["zwave_js_server"], - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.67.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.67.1"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index 8dcc7478d7c..1c7280547c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3215,7 +3215,7 @@ ziggo-mediabox-xl==1.1.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.67.0 +zwave-js-server-python==0.67.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce01dbd5a51..4aa7a5063e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2650,7 +2650,7 @@ zeversolar==0.3.2 zha==0.0.65 # homeassistant.components.zwave_js -zwave-js-server-python==0.67.0 +zwave-js-server-python==0.67.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3